/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "iv/markdown/iv_markdown_article_selection.h" #include "iv/markdown/iv_markdown_prepare_links.h" #include "ui/text/text_utilities.h" #include "ui/widgets/fields/input_field.h" #include "styles/style_iv.h" #include namespace Iv::Markdown { namespace { constexpr auto kCodeTabColumns = 4; const auto kPhotoCopyLabel = u"Photo"_q; [[nodiscard]] TextForMimeData CopyTextForMathSource(QString source) { const auto length = int(source.size()); auto result = TextForMimeData::Simple(std::move(source)); if (length > 0) { result.tags.push_back({ .offset = 0, .length = length, .id = Ui::InputField::kTagIvMath, }); } return result; } struct TableCopySlot { const LaidOutTableCell *cell = nullptr; bool origin = false; }; [[nodiscard]] auto BuildTableCopyGrid(const LaidOutBlock &block) -> std::vector> { const auto rowCount = int(block.tableRows.size()); const auto columnCount = int(block.tableColumnWidths.size()); auto result = std::vector>( std::max(rowCount, 0), std::vector(std::max(columnCount, 0))); for (auto rowIndex = 0; rowIndex != rowCount; ++rowIndex) { for (const auto &cell : block.tableRows[rowIndex].cells) { const auto fromRow = std::clamp(rowIndex, 0, rowCount); const auto toRow = std::clamp(rowIndex + cell.rowspan, 0, rowCount); const auto fromColumn = std::clamp(cell.column, 0, columnCount); const auto toColumn = std::clamp( cell.column + cell.colspan, 0, columnCount); for (auto currentRow = fromRow; currentRow != toRow; ++currentRow) { for (auto currentColumn = fromColumn; currentColumn != toColumn; ++currentColumn) { auto &slot = result[currentRow][currentColumn]; slot.cell = &cell; slot.origin = (currentRow == rowIndex) && (currentColumn == cell.column); } } } } return result; } [[nodiscard]] QRect ClipRectHorizontally(QRect rect, QRect clip) { if (rect.isEmpty() || clip.isEmpty()) { return QRect(); } const auto left = std::max(rect.x(), clip.x()); const auto right = std::min( rect.x() + rect.width(), clip.x() + clip.width()); return QRect( left, rect.y(), std::max(right - left, 0), rect.height()); } [[nodiscard]] QRect VisibleTextRect(QRect textRect, QRect outerRect) { return ClipRectHorizontally(textRect, outerRect); } void RefreshBlockSegmentRect( const LaidOutBlock &block, SelectableSegment *segment) { if (!segment) { return; } switch (segment->kind) { case SelectableSegmentKind::DisplayMath: segment->outerRect = block.visibleFormulaRect; break; case SelectableSegmentKind::Table: segment->outerRect = block.visibleTableRect; break; case SelectableSegmentKind::Placeholder: case SelectableSegmentKind::Photo: case SelectableSegmentKind::Media: segment->outerRect = block.visibleMediaRect; break; case SelectableSegmentKind::CodeBlock: segment->outerRect = block.outer; segment->textRect = VisibleTextRect( block.textRect, block.contentRect.isEmpty() ? block.outer : block.contentRect); break; case SelectableSegmentKind::TextLeaf: { auto sourceRect = QRect(); auto containerRect = block.outer; auto interactionRect = QRect(); if (segment->leaf == &block.leaf) { sourceRect = block.textRect; if (block.kind == PreparedBlockKind::Details) { interactionRect = block.headerRect; containerRect = block.headerRect; } else if (block.kind == PreparedBlockKind::CodeBlock) { containerRect = block.contentRect; } } else if (segment->leaf == &block.labelLeaf) { sourceRect = block.labelRect; containerRect = block.headerRect.isEmpty() ? block.outer : block.headerRect; } else if (segment->leaf == &block.subtitleLeaf) { sourceRect = block.subtitleRect; containerRect = block.headerRect.isEmpty() ? block.outer : block.headerRect; } else if (segment->leaf == &block.actionLeaf) { sourceRect = block.actionRect; } const auto visibleRect = VisibleTextRect(sourceRect, containerRect); segment->outerRect = interactionRect.isEmpty() ? visibleRect : interactionRect; segment->textRect = visibleRect; } break; } } [[nodiscard]] TextForMimeData CopyTextForDisplayMath(const LaidOutBlock &block) { return CopyTextForMathSource(block.copyText); } [[nodiscard]] TextForMimeData CopyTextForCodeBlock( const LaidOutBlock &block, TextSelection selection = AllTextSelection) { if (selection == AllTextSelection) { auto rich = block.codeText; if (!rich.text.isEmpty()) { rich.entities.push_back(EntityInText( EntityType::Pre, 0, rich.text.size(), block.codeLanguage)); SortEntities(&rich); } return TextForMimeData::Rich(std::move(rich)); } auto from = 0; auto to = 0; auto displayPosition = 0; auto column = 0; auto found = false; const auto &text = block.codeText.text; for (auto i = 0, count = int(text.size()); i != count; ++i) { const auto ch = text[i]; const auto width = (ch == QChar::Tabulation) ? (kCodeTabColumns - (column % kCodeTabColumns)) : 1; const auto nextDisplayPosition = displayPosition + width; if (selection.to <= displayPosition) { break; } if (selection.from < nextDisplayPosition && selection.to > displayPosition) { if (!found) { from = i; found = true; } to = i + 1; } displayPosition = nextDisplayPosition; if (Ui::Text::IsNewline(ch)) { column = 0; } else { column += width; } } if (!found || to <= from) { return TextForMimeData(); } auto rich = Ui::Text::Mid(block.codeText, from, to - from); if (!rich.text.isEmpty()) { rich.entities.push_back(EntityInText( EntityType::Pre, 0, rich.text.size(), block.codeLanguage)); SortEntities(&rich); } return TextForMimeData::Rich(std::move(rich)); } [[nodiscard]] TextForMimeData CopyTextForTable(const LaidOutBlock &block) { const auto rowCount = int(block.tableRows.size()); const auto columnCount = int(block.tableColumnWidths.size()); auto result = TextForMimeData(); const auto grid = BuildTableCopyGrid(block); if (!block.leaf.isEmpty()) { result.append(block.leaf.toTextForMimeData()); if (rowCount > 0 && columnCount > 0) { result.append(u"\n"_q); } } for (auto rowIndex = 0; rowIndex != rowCount; ++rowIndex) { if (rowIndex) { result.append(u"\n"_q); } for (auto column = 0; column != columnCount; ++column) { if (column) { result.append(u"\t"_q); } const auto &slot = grid[rowIndex][column]; if (slot.origin && slot.cell) { result.append(slot.cell->leaf.toTextForMimeData()); } } } return result; } [[nodiscard]] TextForMimeData CopyTextForTableRange( const LaidOutBlock &block, int rowFrom, int rowTill, int columnFrom, int columnTill) { const auto rowCount = int(block.tableRows.size()); const auto columnCount = int(block.tableColumnWidths.size()); rowFrom = std::clamp(rowFrom, 0, rowCount); rowTill = std::clamp(rowTill, 0, rowCount); columnFrom = std::clamp(columnFrom, 0, columnCount); columnTill = std::clamp(columnTill, 0, columnCount); if (rowTill <= rowFrom || columnTill <= columnFrom) { return TextForMimeData(); } const auto grid = BuildTableCopyGrid(block); auto result = TextForMimeData(); auto emitted = std::vector(); for (auto rowIndex = rowFrom; rowIndex != rowTill; ++rowIndex) { if (rowIndex != rowFrom) { result.append(u"\n"_q); } for (auto column = columnFrom; column != columnTill; ++column) { if (column != columnFrom) { result.append(u"\t"_q); } const auto &slot = grid[rowIndex][column]; if (slot.cell && (std::find( emitted.begin(), emitted.end(), slot.cell) == emitted.end())) { result.append(slot.cell->leaf.toTextForMimeData()); emitted.push_back(slot.cell); } } } return result; } [[nodiscard]] TextForMimeData CopyTextForMediaBlock( const QString &label, const Ui::Text::String &captionLeaf) { auto result = label.isEmpty() ? TextForMimeData() : TextForMimeData::Simple(label); if (!captionLeaf.isEmpty()) { if (!result.empty()) { result.append(u"\n"_q); } result.append(captionLeaf.toTextForMimeData()); } return result; } [[nodiscard]] TextForMimeData CopyTextForSingleMediaBlock( const LaidOutBlock &block, const QString &fallback = QString()) { const auto selectionData = block.mediaBlock ? block.mediaBlock->selectionData() : MediaBlockSelectionData(); const auto label = !selectionData.copyText.isEmpty() ? selectionData.copyText : !block.copyText.isEmpty() ? block.copyText : !block.labelText.isEmpty() ? block.labelText : fallback; return CopyTextForMediaBlock(label, block.leaf); } [[nodiscard]] TextForMimeData CopyTextForPhotoBlock(const LaidOutBlock &block) { return CopyTextForSingleMediaBlock(block, kPhotoCopyLabel); } [[nodiscard]] TextForMimeData CopyTextForPlaceholderBlock( const LaidOutBlock &block) { return CopyTextForMediaBlock( block.labelText.isEmpty() ? block.copyText : block.labelText, block.leaf); } [[nodiscard]] int AddSelectableSegment( std::vector *segments, SelectableSegment segment) { segment.index = int(segments->size()); segment.length = std::max(segment.length, 0); segments->push_back(std::move(segment)); return segment.index; } [[nodiscard]] int CompareSelectionPositions( MarkdownArticleSelectionPosition a, MarkdownArticleSelectionPosition b) { if (a.segment != b.segment) { return (a.segment < b.segment) ? -1 : 1; } if (a.offset != b.offset) { return (a.offset < b.offset) ? -1 : 1; } return 0; } [[nodiscard]] MarkdownArticleSelection NormalizeSelection( MarkdownArticleSelection selection) { if (selection.empty()) { return {}; } if (CompareSelectionPositions(selection.from, selection.to) > 0) { std::swap(selection.from, selection.to); } return selection; } [[nodiscard]] int LastTableCellSegmentIndex( const std::vector *segments, int tableSegmentIndex) { auto result = tableSegmentIndex; if (!segments) { return result; } for (const auto &segment : *segments) { if (segment.tableSegmentIndex == tableSegmentIndex) { result = std::max(result, segment.index); } } return result; } [[nodiscard]] std::optional SingleTableCellSelection( const PaintSelectionState &selectionState, int tableSegmentIndex) { if (selectionState.empty() || !selectionState.endpoints || tableSegmentIndex < 0) { return std::nullopt; } const auto normalized = NormalizeSelection(selectionState.selection); if (normalized.empty()) { return std::nullopt; } const auto lastCellSegment = LastTableCellSegmentIndex( selectionState.segments, tableSegmentIndex); const auto spansWholeTable = (normalized.from.segment < tableSegmentIndex) && (normalized.to.segment > lastCellSegment); auto tableHit = false; auto cellSegment = -1; auto multipleCells = false; const auto consider = [&](MarkdownArticleSelectionEndpoint endpoint) { if (!endpoint.valid() || !endpoint.direct) { return; } const auto segment = FindSegment(selectionState.segments, endpoint.segment); if (!segment) { return; } if (segment->index == tableSegmentIndex) { tableHit = true; return; } if (segment->tableSegmentIndex != tableSegmentIndex) { return; } if (cellSegment < 0) { cellSegment = segment->index; } else if (cellSegment != segment->index) { multipleCells = true; } }; consider(selectionState.endpoints->from); consider(selectionState.endpoints->to); if (tableHit || multipleCells || cellSegment < 0 || spansWholeTable) { return std::nullopt; } return cellSegment; } [[nodiscard]] std::optional BaseTextSelectionForSegment( const SelectableSegment &segment, MarkdownArticleSelection selection) { if (selection.empty() || !segment.isTextLeaf()) { return std::nullopt; } selection = NormalizeSelection(selection); if (selection.empty() || selection.from.segment > segment.index || selection.to.segment < segment.index) { return std::nullopt; } auto from = 0; auto to = SegmentLength(segment); if (selection.from.segment == segment.index) { from = selection.from.offset; } if (selection.to.segment == segment.index) { to = selection.to.offset; } from = std::clamp(from, 0, SegmentLength(segment)); to = std::clamp(to, 0, SegmentLength(segment)); if (from >= to) { return std::nullopt; } return TextSelection(uint16(from), uint16(to)); } [[nodiscard]] bool RangeSelectsWholeSegment( const SelectableSegment &segment, MarkdownArticleSelection selection) { selection = NormalizeSelection(selection); if (selection.empty() || selection.from.segment > segment.index || selection.to.segment < segment.index) { return false; } auto from = 0; auto to = SegmentLength(segment); if (selection.from.segment == segment.index) { from = selection.from.offset; } if (selection.to.segment == segment.index) { to = selection.to.offset; } from = std::clamp(from, 0, SegmentLength(segment)); to = std::clamp(to, 0, SegmentLength(segment)); return (from < to); } [[nodiscard]] bool IndexInRange(int index, int from, int till) { return (index >= from) && (index < till); } [[nodiscard]] bool RangesIntersect( int firstFrom, int firstTill, int secondFrom, int secondTill) { return (firstFrom < secondTill) && (secondFrom < firstTill); } [[nodiscard]] const PreparedEditSelection *StructuralSelection( const PaintSelectionState &selectionState, PreparedEditSelectionKind kind) { if (!selectionState.hasStructuralSelection()) { return nullptr; } const auto selection = selectionState.structuralSelection; return (selection->kind == kind) ? selection : nullptr; } [[nodiscard]] bool ContainerHasPrefix( const PreparedEditBlockContainerPath &path, const PreparedEditBlockContainerPath &prefix) { if (path.steps.size() < prefix.steps.size()) { return false; } return std::equal( prefix.steps.begin(), prefix.steps.end(), path.steps.begin()); } [[nodiscard]] bool PathInBlockRange( const PreparedEditBlockPath &path, const PreparedEditBlockRange &range) { if (path.container == range.container) { return IndexInRange(path.index, range.from, range.till); } if (!ContainerHasPrefix(path.container, range.container) || (path.container.steps.size() <= range.container.steps.size())) { return false; } const auto &step = path.container.steps[range.container.steps.size()]; return IndexInRange(step.blockIndex, range.from, range.till); } [[nodiscard]] bool PathInListItemRange( const PreparedEditBlockPath &path, const PreparedEditListItemRange &range) { if (!ContainerHasPrefix(path.container, range.block.container) || (path.container.steps.size() <= range.block.container.steps.size())) { return false; } const auto &step = path.container.steps[range.block.container.steps.size()]; return (step.kind == PreparedEditBlockContainerKind::ListItemChildren) && (step.blockIndex == range.block.index) && IndexInRange(step.listItemIndex, range.from, range.till); } [[nodiscard]] bool BlockSourceInStructuralSelection( const PreparedEditBlockSource &source, const PreparedEditSelection &selection) { switch (selection.kind) { case PreparedEditSelectionKind::Blocks: return PathInBlockRange(source.path, selection.blocks); case PreparedEditSelectionKind::ListItems: return PathInListItemRange(source.path, selection.listItems); case PreparedEditSelectionKind::TableRows: case PreparedEditSelectionKind::TableCells: case PreparedEditSelectionKind::None: return false; } return false; } [[nodiscard]] bool ListItemSourceInStructuralSelection( const PreparedEditListItemSource &source, const PreparedEditSelection &selection) { switch (selection.kind) { case PreparedEditSelectionKind::Blocks: return PathInBlockRange(source.block, selection.blocks); case PreparedEditSelectionKind::ListItems: return (source.block == selection.listItems.block) && IndexInRange( source.listItemIndex, selection.listItems.from, selection.listItems.till); case PreparedEditSelectionKind::TableRows: case PreparedEditSelectionKind::TableCells: case PreparedEditSelectionKind::None: return false; } return false; } [[nodiscard]] bool BlockBelongsToStructuralSelection( const LaidOutBlock &block, const PreparedEditSelection &selection) { return (block.editListItem && ListItemSourceInStructuralSelection(*block.editListItem, selection)) || (block.editBlock && BlockSourceInStructuralSelection(*block.editBlock, selection)); } [[nodiscard]] bool WholeSegmentStructurallySelected( const SelectableSegment &segment, const PaintSelectionState &selectionState) { if (!selectionState.hasStructuralSelection() || !segment.block) { return false; } switch (segment.kind) { case SelectableSegmentKind::CodeBlock: case SelectableSegmentKind::DisplayMath: case SelectableSegmentKind::Table: case SelectableSegmentKind::Placeholder: case SelectableSegmentKind::Photo: case SelectableSegmentKind::Media: return BlockBelongsToStructuralSelection( *segment.block, *selectionState.structuralSelection); case SelectableSegmentKind::TextLeaf: return false; } return false; } [[nodiscard]] std::optional StructuralTextSelectionForSegment( const SelectableSegment &segment, const PaintSelectionState &selectionState) { if (!selectionState.hasStructuralSelection() || !segment.isTextLeaf() || !segment.block) { return std::nullopt; } switch (selectionState.structuralSelection->kind) { case PreparedEditSelectionKind::Blocks: case PreparedEditSelectionKind::ListItems: return BlockBelongsToStructuralSelection( *segment.block, *selectionState.structuralSelection) ? std::make_optional(AllTextSelection) : std::nullopt; case PreparedEditSelectionKind::TableRows: case PreparedEditSelectionKind::TableCells: case PreparedEditSelectionKind::None: return std::nullopt; } return std::nullopt; } [[nodiscard]] std::optional ResolveTextSelectionForSegment( const SelectableSegment &segment, const PaintSelectionState &selectionState, bool suppressStructuralSelection) { if (!selectionState.segments) { return std::nullopt; } if (segment.tableSegmentIndex >= 0) { if (const auto singleCell = SingleTableCellSelection( selectionState, segment.tableSegmentIndex); singleCell && *singleCell != segment.index) { return std::nullopt; } } if (segment.tableSegmentIndex >= 0 && TableSegmentSelected(selectionState, segment.tableSegmentIndex)) { return std::nullopt; } if (const auto structural = StructuralTextSelectionForSegment( segment, selectionState)) { return suppressStructuralSelection ? std::nullopt : structural; } return BaseTextSelectionForSegment(segment, selectionState.selection); } [[nodiscard]] const LaidOutBlock *FindTableBlock( const std::vector &segments, const PreparedEditBlockPath &path) { for (const auto &segment : segments) { if (!segment.block || (segment.block->kind != PreparedBlockKind::Table) || !segment.block->editBlock || (segment.block->editBlock->path != path)) { continue; } return segment.block; } return nullptr; } [[nodiscard]] TextForMimeData TextForStructuralTableSelection( const std::vector &segments, const PreparedEditSelection &selection) { switch (selection.kind) { case PreparedEditSelectionKind::TableRows: { const auto block = FindTableBlock(segments, selection.tableRows.block); return block ? CopyTextForTableRange( *block, selection.tableRows.from, selection.tableRows.till, 0, int(block->tableColumnWidths.size())) : TextForMimeData(); } case PreparedEditSelectionKind::TableCells: { const auto block = FindTableBlock(segments, selection.tableCells.block); return block ? CopyTextForTableRange( *block, selection.tableCells.rowFrom, selection.tableCells.rowTill, selection.tableCells.columnFrom, selection.tableCells.columnTill) : TextForMimeData(); } case PreparedEditSelectionKind::Blocks: case PreparedEditSelectionKind::ListItems: case PreparedEditSelectionKind::None: return TextForMimeData(); } return TextForMimeData(); } [[nodiscard]] const style::TextStyle &HeadingTextStyle( int level, const style::Markdown &st) { switch (std::clamp(level, 1, 6)) { case 1: return st.heading1; case 2: return st.heading2; case 3: return st.heading3; case 4: return st.heading4; case 5: return st.heading5; case 6: return st.heading6; } return st.heading6; } [[nodiscard]] bool IsEmbedPostAuthorSegment( const SelectableSegment &segment) { return segment.block && (segment.block->kind == PreparedBlockKind::EmbedPost) && (segment.leaf == &segment.block->labelLeaf); } [[nodiscard]] bool IsEmbedPostDateSegment( const SelectableSegment &segment) { return segment.block && (segment.block->kind == PreparedBlockKind::EmbedPost) && (segment.leaf == &segment.block->subtitleLeaf); } } // namespace void CollectSelectableSegments( std::vector *blocks, std::vector *segments) { if (!blocks) { return; } for (auto &block : *blocks) { block.segmentIndex = -1; block.secondarySegmentIndex = -1; block.tertiarySegmentIndex = -1; switch (block.kind) { case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::Details: { if (block.leaf.isEmpty() && block.textRect.isEmpty()) { break; } auto segment = SelectableSegment(); segment.kind = SelectableSegmentKind::TextLeaf; segment.leaf = &block.leaf; segment.block = █ segment.outerRect = (block.kind == PreparedBlockKind::Details) ? block.headerRect : block.textRect; segment.textRect = block.textRect; segment.textWidth = block.textWidth; segment.align = block.flowTextAlign; segment.length = block.leaf.length(); block.segmentIndex = AddSelectableSegment( segments, std::move(segment)); } break; case PreparedBlockKind::CodeBlock: { auto segment = SelectableSegment(); segment.kind = SelectableSegmentKind::CodeBlock; segment.leaf = &block.leaf; segment.block = █ segment.outerRect = block.outer; segment.textRect = block.textRect; segment.textWidth = block.textWidth; segment.length = block.leaf.length(); block.segmentIndex = AddSelectableSegment( segments, std::move(segment)); } break; case PreparedBlockKind::DisplayMath: { auto segment = SelectableSegment(); segment.kind = SelectableSegmentKind::DisplayMath; segment.block = █ segment.outerRect = block.visibleFormulaRect; segment.align = block.formulaAlign; segment.length = 1; block.segmentIndex = AddSelectableSegment( segments, std::move(segment)); } break; case PreparedBlockKind::Table: { if (!block.textRect.isEmpty()) { auto textSegment = SelectableSegment(); textSegment.kind = SelectableSegmentKind::TextLeaf; textSegment.leaf = &block.leaf; textSegment.block = █ textSegment.outerRect = block.textRect; textSegment.textRect = block.textRect; textSegment.textWidth = block.textWidth; textSegment.align = block.flowTextAlign; textSegment.length = block.leaf.length(); block.secondarySegmentIndex = AddSelectableSegment( segments, std::move(textSegment)); } auto segment = SelectableSegment(); segment.kind = SelectableSegmentKind::Table; segment.block = █ segment.outerRect = block.visibleTableRect; segment.length = 1; block.segmentIndex = AddSelectableSegment( segments, std::move(segment)); if (block.secondarySegmentIndex >= 0) { (*segments)[block.secondarySegmentIndex].parentSegmentIndex = block.segmentIndex; } for (auto &row : block.tableRows) { for (auto &cell : row.cells) { auto cellSegment = SelectableSegment(); cellSegment.kind = SelectableSegmentKind::TextLeaf; cellSegment.leaf = &cell.leaf; cellSegment.block = █ cellSegment.cell = &cell; cellSegment.outerRect = cell.outer; cellSegment.textRect = cell.textRect; cellSegment.textWidth = cell.textWidth; cellSegment.align = cell.align; cellSegment.length = cell.leaf.length(); cellSegment.tableSegmentIndex = block.segmentIndex; cell.tableSegmentIndex = block.segmentIndex; cell.segmentIndex = AddSelectableSegment( segments, std::move(cellSegment)); } } } break; case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::Placeholder: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: { auto segment = SelectableSegment(); segment.kind = (block.kind == PreparedBlockKind::Photo) ? SelectableSegmentKind::Photo : (block.kind == PreparedBlockKind::Placeholder) ? SelectableSegmentKind::Placeholder : SelectableSegmentKind::Media; segment.block = █ segment.outerRect = block.mediaRect; segment.length = 1; block.segmentIndex = AddSelectableSegment( segments, std::move(segment)); if (!block.textRect.isEmpty()) { auto textSegment = SelectableSegment(); textSegment.kind = SelectableSegmentKind::TextLeaf; textSegment.leaf = &block.leaf; textSegment.block = █ textSegment.outerRect = block.textRect; textSegment.textRect = block.textRect; textSegment.textWidth = block.textWidth; textSegment.length = block.leaf.length(); textSegment.parentSegmentIndex = block.segmentIndex; block.secondarySegmentIndex = AddSelectableSegment( segments, std::move(textSegment)); } } break; case PreparedBlockKind::EmbedPost: { if (!block.labelRect.isEmpty() && !block.labelLeaf.isEmpty()) { auto authorSegment = SelectableSegment(); authorSegment.kind = SelectableSegmentKind::TextLeaf; authorSegment.leaf = &block.labelLeaf; authorSegment.block = █ authorSegment.outerRect = block.labelRect; authorSegment.textRect = block.labelRect; authorSegment.textWidth = block.labelWidth; authorSegment.length = block.labelLeaf.length(); block.segmentIndex = AddSelectableSegment( segments, std::move(authorSegment)); } if (!block.subtitleRect.isEmpty() && !block.subtitleLeaf.isEmpty()) { auto dateSegment = SelectableSegment(); dateSegment.kind = SelectableSegmentKind::TextLeaf; dateSegment.leaf = &block.subtitleLeaf; dateSegment.block = █ dateSegment.outerRect = block.subtitleRect; dateSegment.textRect = block.subtitleRect; dateSegment.textWidth = block.subtitleWidth; dateSegment.length = block.subtitleLeaf.length(); block.secondarySegmentIndex = AddSelectableSegment( segments, std::move(dateSegment)); } CollectSelectableSegments(&block.children, segments); if (!block.textRect.isEmpty() && !block.leaf.isEmpty()) { auto captionSegment = SelectableSegment(); captionSegment.kind = SelectableSegmentKind::TextLeaf; captionSegment.leaf = &block.leaf; captionSegment.block = █ captionSegment.outerRect = block.textRect; captionSegment.textRect = block.textRect; captionSegment.textWidth = block.textWidth; captionSegment.length = block.leaf.length(); block.tertiarySegmentIndex = AddSelectableSegment( segments, std::move(captionSegment)); } continue; } case PreparedBlockKind::List: case PreparedBlockKind::ListItem: case PreparedBlockKind::Quote: case PreparedBlockKind::Rule: break; } CollectSelectableSegments(&block.children, segments); } } void RefreshScrollableSegmentRects( const std::vector &blocks, std::vector *segments) { if (!segments) { return; } for (const auto &block : blocks) { RefreshScrollableSegmentRects(block, segments); } } void RefreshScrollableSegmentRects( const LaidOutBlock &block, std::vector *segments) { if (!segments) { return; } const auto refreshIndex = [&](int index) { if (index < 0 || index >= int(segments->size())) { return; } RefreshBlockSegmentRect(block, &(*segments)[index]); }; refreshIndex(block.segmentIndex); refreshIndex(block.secondarySegmentIndex); refreshIndex(block.tertiarySegmentIndex); if (block.kind == PreparedBlockKind::Table) { for (const auto &row : block.tableRows) { for (const auto &cell : row.cells) { if (cell.segmentIndex < 0 || cell.segmentIndex >= int(segments->size())) { continue; } auto &segment = (*segments)[cell.segmentIndex]; segment.outerRect = cell.outer; segment.textRect = VisibleTextRect( cell.textRect, cell.outer); } } } RefreshScrollableSegmentRects(block.children, segments); } void CollectAnchors( const std::vector &blocks, std::vector> *anchors) { if (!anchors) { return; } for (const auto &block : blocks) { if (!block.anchorId.isEmpty() || !block.anchorIds.empty()) { const auto top = !block.textRect.isEmpty() ? block.textRect.top() : [&] { for (const auto &child : block.children) { if (child.outer.height() > 0) { return child.outer.top(); } } return block.outer.top(); }(); if (!block.anchorId.isEmpty()) { anchors->push_back({ block.anchorId, top }); } for (const auto &anchorId : block.anchorIds) { anchors->push_back({ anchorId, top }); } } CollectAnchors(block.children, anchors); } } const SelectableSegment *FindSegment( const std::vector *segments, int index) { if (!segments || index < 0 || index >= int(segments->size())) { return nullptr; } return &(*segments)[index]; } int SegmentLength(const SelectableSegment &segment) { return std::max(segment.length, 0); } const style::TextStyle &TextStyleForSegment( const SelectableSegment &segment, const style::Markdown &st) { if (segment.cell) { return segment.cell->header ? st.table.headerStyle : st.table.bodyStyle; } else if (IsEmbedPostAuthorSegment(segment)) { return st.embedPost.authorStyle; } else if (IsEmbedPostDateSegment(segment)) { return st.embedPost.dateStyle; } else if (segment.kind == SelectableSegmentKind::CodeBlock) { return st.code; } else if (segment.block && segment.block->quoteAuthor) { return st.quoteAuthorStyle; } else if (!segment.block) { return st.body; } switch (segment.block->kind) { case PreparedBlockKind::Details: return st.details.summaryStyle; case PreparedBlockKind::CodeBlock: return st.code; case PreparedBlockKind::Heading: return HeadingTextStyle(segment.block->headingLevel, st); default: return st.body; } } style::color TextColorForSegment( const SelectableSegment &segment, const style::Markdown &st) { if (IsEmbedPostAuthorSegment(segment)) { return st.embedPost.authorFg; } else if (IsEmbedPostDateSegment(segment)) { return st.embedPost.dateFg; } else if (segment.block && (segment.block->kind == PreparedBlockKind::Thinking)) { return st.supplementaryTextColor; } else if (segment.kind == SelectableSegmentKind::CodeBlock || (segment.block && segment.block->kind == PreparedBlockKind::CodeBlock)) { return st.textPalette.monoFg; } else if (segment.block && segment.block->supplementary) { return st.supplementaryTextColor; } return st.textColor; } bool TableSegmentSelected( const PaintSelectionState &selectionState, int tableSegmentIndex) { if (!selectionState.segments || tableSegmentIndex < 0) { return false; } if (selectionState.hasStructuralSelection()) { if (const auto segment = FindSegment( selectionState.segments, tableSegmentIndex); segment && WholeSegmentStructurallySelected( *segment, selectionState)) { return true; } } if (SingleTableCellSelection(selectionState, tableSegmentIndex)) { return false; } const auto normalized = NormalizeSelection(selectionState.selection); if (normalized.empty()) { return false; } auto selectedCells = 0; auto selectedCellIndex = -1; for (const auto &segment : *selectionState.segments) { if (segment.tableSegmentIndex != tableSegmentIndex || segment.index == tableSegmentIndex) { continue; } const auto textSelection = BaseTextSelectionForSegment( segment, normalized); if (!textSelection || textSelection->empty()) { continue; } if (++selectedCells == 1) { selectedCellIndex = segment.index; } else { return true; } } const auto table = FindSegment(selectionState.segments, tableSegmentIndex); if (!table || !RangeSelectsWholeSegment(*table, normalized)) { return false; } if (selectedCells != 1) { return true; } if (normalized.from.segment == tableSegmentIndex || normalized.to.segment == tableSegmentIndex) { return true; } const auto lower = std::min(tableSegmentIndex, selectedCellIndex); const auto upper = std::max(tableSegmentIndex, selectedCellIndex); return (normalized.from.segment < lower) && (normalized.to.segment > upper); } bool StructuralBlockSelected( const PaintSelectionState &selectionState, const PreparedEditBlockSource &source) { const auto selection = StructuralSelection( selectionState, PreparedEditSelectionKind::Blocks); if (!selection) { return false; } const auto &range = selection->blocks; return (source.path.container == range.container) && IndexInRange(source.path.index, range.from, range.till); } bool StructuralListItemSelected( const PaintSelectionState &selectionState, const PreparedEditListItemSource &source) { const auto selection = StructuralSelection( selectionState, PreparedEditSelectionKind::ListItems); if (!selection) { return false; } const auto &range = selection->listItems; return (source.block == range.block) && IndexInRange(source.listItemIndex, range.from, range.till); } bool StructuralTableRowSelected( const PaintSelectionState &selectionState, const PreparedEditTableRowSource &source) { const auto selection = StructuralSelection( selectionState, PreparedEditSelectionKind::TableRows); if (!selection) { return false; } const auto &range = selection->tableRows; return (source.block == range.block) && IndexInRange(source.tableRowIndex, range.from, range.till); } bool StructuralTableCellSelected( const PaintSelectionState &selectionState, const PreparedEditTableCellSource &source) { const auto selection = StructuralSelection( selectionState, PreparedEditSelectionKind::TableCells); if (!selection) { return false; } const auto &range = selection->tableCells; if (range.empty() || source.tableRowIndex < 0 || source.column < 0 || source.colspan <= 0 || source.rowspan <= 0) { return false; } return (source.block == range.block) && RangesIntersect( source.tableRowIndex, source.tableRowIndex + source.rowspan, range.rowFrom, range.rowTill) && RangesIntersect( source.column, source.column + source.colspan, range.columnFrom, range.columnTill); } std::optional TextSelectionForSegment( const SelectableSegment &segment, const PaintSelectionState &selectionState) { return ResolveTextSelectionForSegment( segment, selectionState, false); } std::optional TextSelectionForSegmentIndex( const PaintSelectionState &selectionState, int index) { const auto segment = FindSegment(selectionState.segments, index); return segment ? TextSelectionForSegment(*segment, selectionState) : std::nullopt; } std::optional PaintTextSelectionForSegment( const SelectableSegment &segment, const PaintSelectionState &selectionState) { return ResolveTextSelectionForSegment( segment, selectionState, true); } std::optional PaintTextSelectionForSegmentIndex( const PaintSelectionState &selectionState, int index) { const auto segment = FindSegment(selectionState.segments, index); return segment ? PaintTextSelectionForSegment(*segment, selectionState) : std::nullopt; } bool WholeSegmentSelected( const SelectableSegment &segment, const PaintSelectionState &selectionState) { if (!selectionState.segments || segment.isTextLeaf()) { return false; } if (WholeSegmentStructurallySelected(segment, selectionState)) { return true; } if (segment.kind == SelectableSegmentKind::Table) { return TableSegmentSelected(selectionState, segment.index); } return RangeSelectsWholeSegment(segment, selectionState.selection); } bool WholeSegmentSelected( const PaintSelectionState &selectionState, int index) { const auto segment = FindSegment(selectionState.segments, index); return segment ? WholeSegmentSelected(*segment, selectionState) : false; } TextForMimeData TextForSegment( const SelectableSegment &segment, TextSelection selection) { switch (segment.kind) { case SelectableSegmentKind::TextLeaf: return segment.leaf ? segment.leaf->toTextForMimeData(selection) : TextForMimeData(); case SelectableSegmentKind::CodeBlock: return segment.block ? CopyTextForCodeBlock(*segment.block, selection) : TextForMimeData(); case SelectableSegmentKind::DisplayMath: return segment.block ? CopyTextForDisplayMath(*segment.block) : TextForMimeData(); case SelectableSegmentKind::Table: return segment.block ? CopyTextForTable(*segment.block) : TextForMimeData(); case SelectableSegmentKind::Placeholder: return segment.block ? CopyTextForPlaceholderBlock(*segment.block) : TextForMimeData(); case SelectableSegmentKind::Photo: return segment.block ? CopyTextForPhotoBlock(*segment.block) : TextForMimeData(); case SelectableSegmentKind::Media: return segment.block ? CopyTextForSingleMediaBlock(*segment.block) : TextForMimeData(); } return TextForMimeData(); } TextForMimeData TextForSelectedSegments( const std::vector &segments, MarkdownArticleSelection selection, const MarkdownArticleSelectionEndpoints *endpoints, const PreparedEditSelection *structuralSelection) { if (structuralSelection && !structuralSelection->empty() && (structuralSelection->kind == PreparedEditSelectionKind::TableRows || structuralSelection->kind == PreparedEditSelectionKind::TableCells)) { return TextForStructuralTableSelection(segments, *structuralSelection); } if (selection.empty() && (!structuralSelection || structuralSelection->empty())) { return TextForMimeData(); } const auto selectionState = PaintSelectionState{ .segments = &segments, .selection = selection, .endpoints = endpoints, .structuralSelection = structuralSelection, }; auto pieces = std::vector(); for (const auto &segment : segments) { if (segment.isTextLeaf()) { if (segment.parentSegmentIndex >= 0 && WholeSegmentSelected( selectionState, segment.parentSegmentIndex)) { continue; } if (const auto textSelection = TextSelectionForSegment( segment, selectionState); textSelection && !textSelection->empty()) { if (auto text = TextForSegment(segment, *textSelection); !text.empty()) { pieces.push_back(std::move(text)); } } continue; } if (!WholeSegmentSelected(segment, selectionState)) { continue; } if (auto text = TextForSegment(segment); !text.empty()) { pieces.push_back(std::move(text)); } } if (pieces.empty()) { return TextForMimeData(); } else if (pieces.size() == 1) { return std::move(pieces.front()); } auto result = TextForMimeData(); for (auto i = 0, count = int(pieces.size()); i != count; ++i) { if (i) { result.append(u"\n"_q); } result.append(std::move(pieces[i])); } return result; } } // namespace Iv::Markdown