/* 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.h" #include "base/algorithm.h" #include "iv/markdown/iv_markdown_article_layout_structure.h" #include "iv/markdown/iv_markdown_article_paint.h" #include "iv/markdown/iv_markdown_article_selection.h" #include "iv/markdown/iv_markdown_article_text.h" #include "iv/markdown/iv_markdown_media_reuse.h" #include "iv/markdown/iv_markdown_prepare_links.h" #include "iv/markdown/iv_markdown_prepare_serialize.h" #include "lang/lang_keys.h" #include "ui/style/style_core_color.h" #include "ui/style/style_core_scale.h" #include "ui/widgets/checkbox.h" #include "ui/basic_click_handlers.h" #include "ui/dynamic_image.h" #include "styles/style_iv.h" #include "styles/style_widgets.h" #include #include #include #include #include #include #include namespace Iv::Markdown { namespace { struct PendingHighlightKey { QString text; QString language; }; [[nodiscard]] bool operator==( const PendingHighlightKey &a, const PendingHighlightKey &b) { return (a.text == b.text) && (a.language == b.language); } struct PendingHighlightKeyHasher { [[nodiscard]] size_t operator()( const PendingHighlightKey &key) const noexcept; }; size_t PendingHighlightKeyHasher::operator()( const PendingHighlightKey &key) const noexcept { auto result = size_t(qHash(key.text)); result = (result * 1315423911U) ^ size_t(qHash(key.language)); return result; } struct PendingHighlightEntry { PendingHighlightKey key; std::vector blocks; }; struct RelatedArticleImageState { std::shared_ptr thumbnailImage; std::shared_ptr previousThumbnailImage; std::shared_ptr fullImage; std::shared_ptr previousFullImage; }; [[nodiscard]] size_t CombineHash(size_t accumulator, size_t value) { return (accumulator * 1315423911U) ^ value; } struct MarkdownArticleScrollOwnerIdentity { std::optional blockPath; std::vector preparedPath; friend inline bool operator==( const MarkdownArticleScrollOwnerIdentity &a, const MarkdownArticleScrollOwnerIdentity &b) { return (a.blockPath == b.blockPath) && (a.preparedPath == b.preparedPath); } }; struct MarkdownArticleScrollOwnerIdentityHasher { [[nodiscard]] size_t operator()( const MarkdownArticleScrollOwnerIdentity &value) const noexcept { auto result = CombineHash(0, value.blockPath ? 1 : 0); if (value.blockPath) { for (const auto &step : value.blockPath->container.steps) { result = CombineHash( result, static_cast(step.kind)); result = CombineHash( result, size_t(step.blockIndex + 1)); result = CombineHash( result, size_t(step.listItemIndex + 1)); } result = CombineHash( result, size_t(value.blockPath->index + 1)); } result = CombineHash(result, size_t(value.preparedPath.size() + 1)); for (const auto step : value.preparedPath) { result = CombineHash(result, size_t(step + 1)); } return result; } }; struct MarkdownArticleHorizontalScrollLookup { MarkdownArticleHorizontalScrollHit hit; MarkdownArticleScrollOwnerIdentity identity; const LaidOutBlock *block = nullptr; }; [[nodiscard]] bool TextDependsOnMediaRuntime( const TextWithEntities &text) { for (const auto &entity : text.entities) { if (entity.type() != EntityType::CustomEmoji) { continue; } const auto parsed = ParseInlineTextObjectEntity(entity.data()); if (parsed && (parsed->kind == InlineTextObjectKind::IvImage)) { return true; } } return false; } [[nodiscard]] TextWithEntities DisplayMathFallbackText() { auto result = TextWithEntities::Simple(u"Invalid formula"_q); result.entities.push_back(EntityInText( EntityType::Italic, 0, result.text.size())); return result; } [[nodiscard]] QString DetailsStateText(bool open) { return open ? tr::lng_iv_details_state_expanded(tr::now) : tr::lng_iv_details_state_collapsed(tr::now); } [[nodiscard]] size_t TextStyleKey(const style::TextStyle &style) { return reinterpret_cast(&style); } [[nodiscard]] CachedTextLeafSourceSignature MarkedTextLeafSourceSignature( TextWithEntities text, const style::TextStyle &textStyle, int minResizeWidth) { auto result = CachedTextLeafSourceSignature(); result.dependsOnMediaRuntime = TextDependsOnMediaRuntime(text); result.text = std::move(text); result.minResizeWidth = minResizeWidth; result.styleKey = TextStyleKey(textStyle); return result; } [[nodiscard]] CachedTextLeafSourceSignature PlainTextLeafSourceSignature( const QString &text, const style::TextStyle &textStyle, int minResizeWidth) { return MarkedTextLeafSourceSignature( TextWithEntities::Simple(text), textStyle, minResizeWidth); } [[nodiscard]] CachedTextLeafSourceSignature CodeTextLeafSourceSignature( const PreparedBlock &prepared, const style::Markdown &st) { auto result = MarkedTextLeafSourceSignature( CodeBlockDisplayText(prepared.text), st.code, CodeTextMinResizeWidth(st)); result.codeLanguage = prepared.codeLanguage; return result; } [[nodiscard]] CachedTextLeafKey BlockCachedTextLeafKey( CachedTextLeafSlot slot, const PreparedBlock &prepared, const std::vector &preparedPath) { auto result = CachedTextLeafKey(); result.slot = slot; if ((slot == CachedTextLeafSlot::Leaf || slot == CachedTextLeafSlot::Placeholder || slot == CachedTextLeafSlot::Fallback) && prepared.editLeaf) { result.identityKind = CachedTextLeafIdentityKind::EditLeaf; result.editLeaf = *prepared.editLeaf; return result; } if ((slot == CachedTextLeafSlot::Marker) && prepared.editListItem) { result.identityKind = CachedTextLeafIdentityKind::EditListItem; result.editListItem = *prepared.editListItem; return result; } if (prepared.editBlock) { result.identityKind = CachedTextLeafIdentityKind::EditBlock; result.editBlock = *prepared.editBlock; return result; } if (prepared.editListItem) { result.identityKind = CachedTextLeafIdentityKind::EditListItem; result.editListItem = *prepared.editListItem; return result; } if (prepared.editLeaf) { result.identityKind = CachedTextLeafIdentityKind::EditLeaf; result.editLeaf = *prepared.editLeaf; return result; } result.preparedPath = preparedPath; return result; } [[nodiscard]] CachedTextLeafKey TableCellCachedTextLeafKey( CachedTextLeafSlot slot, const PreparedTableCell &prepared, const std::vector &preparedPath, int tableRowIndex, int tableCellIndex) { auto result = CachedTextLeafKey(); result.slot = slot; result.tableRowIndex = tableRowIndex; result.tableCellIndex = tableCellIndex; if (prepared.editLeaf) { result.identityKind = CachedTextLeafIdentityKind::EditLeaf; result.editLeaf = *prepared.editLeaf; return result; } if (prepared.editCell) { result.identityKind = CachedTextLeafIdentityKind::EditTableCell; result.editTableCell = *prepared.editCell; return result; } result.preparedPath = preparedPath; return result; } void StoreCachedTextLeaf( CachedTextLeafPool *pool, CachedTextLeafKey key, CachedTextLeafSourceSignature source, Ui::Text::String *leaf, Spellchecker::HighlightProcessId syntaxHighlightProcessId = 0) { if (!pool || !leaf || leaf->isEmpty()) { return; } pool->entries.insert_or_assign( std::move(key), CachedTextLeafEntry{ .leaf = std::move(*leaf), .source = std::move(source), .syntaxHighlightProcessId = syntaxHighlightProcessId, }); *leaf = Ui::Text::String(); } void PruneMediaRuntimeBoundCachedTextLeafs(CachedTextLeafPool *pool) { if (!pool) { return; } for (auto i = pool->entries.begin(); i != pool->entries.end();) { if (i->second.source.dependsOnMediaRuntime) { i = pool->entries.erase(i); } else { ++i; } } } void HarvestCachedTextLeafs( const std::vector &preparedBlocks, std::vector *blocks, const style::Markdown &st, CachedTextLeafPool *pool, std::vector *preparedPath); void RebuildCachedTextLeafs( const std::vector &preparedBlocks, std::vector *blocks, const style::Markdown &st, CachedTextLeafPool *pool) { if (!pool) { return; } pool->entries.clear(); auto preparedPath = std::vector(); HarvestCachedTextLeafs( preparedBlocks, blocks, st, pool, &preparedPath); } void HarvestCachedTextLeafs( const PreparedBlock &prepared, LaidOutBlock *block, const style::Markdown &st, CachedTextLeafPool *pool, const std::vector &preparedPath) { const auto storeBlockLeaf = [&](CachedTextLeafSlot slot, CachedTextLeafSourceSignature source, Ui::Text::String *leaf) { StoreCachedTextLeaf( pool, BlockCachedTextLeafKey(slot, prepared, preparedPath), std::move(source), leaf); }; const auto storeTableCellLeaf = [&]( CachedTextLeafSlot slot, const PreparedTableCell &preparedCell, int tableRowIndex, int tableCellIndex, CachedTextLeafSourceSignature source, Ui::Text::String *leaf) { StoreCachedTextLeaf( pool, TableCellCachedTextLeafKey( slot, preparedCell, preparedPath, tableRowIndex, tableCellIndex), std::move(source), leaf); }; if (!block->marker.isEmpty()) { storeBlockLeaf( CachedTextLeafSlot::Marker, PlainTextLeafSourceSignature( ListMarkerText(prepared), st.body, PlainTextMinResizeWidth(st.body)), &block->marker); } switch (prepared.kind) { case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: { const auto &textStyle = TextStyleFor(prepared, st); const auto &placeholderStyle = EditPlaceholderTextStyleFor( prepared, st); storeBlockLeaf( CachedTextLeafSlot::Leaf, MarkedTextLeafSourceSignature( prepared.text, textStyle, FlowBlockMinimumWidth(prepared, st)), &block->leaf); storeBlockLeaf( CachedTextLeafSlot::Placeholder, PlainTextLeafSourceSignature( prepared.editPlaceholderText, placeholderStyle, PlainTextMinResizeWidth(placeholderStyle)), &block->placeholderLeaf); } break; case PreparedBlockKind::CodeBlock: StoreCachedTextLeaf( pool, BlockCachedTextLeafKey( CachedTextLeafSlot::Leaf, prepared, preparedPath), CodeTextLeafSourceSignature(prepared, st), &block->leaf, block->syntaxHighlightProcessId); block->syntaxHighlightProcessId = 0; storeBlockLeaf( CachedTextLeafSlot::Placeholder, PlainTextLeafSourceSignature( prepared.editPlaceholderText, st.code, PlainTextMinResizeWidth(st.code)), &block->placeholderLeaf); break; case PreparedBlockKind::DisplayMath: storeBlockLeaf( CachedTextLeafSlot::Placeholder, PlainTextLeafSourceSignature( prepared.editPlaceholderText, st.displayMath.fallbackStyle, DisplayMathFallbackTextMinResizeWidth(st)), &block->placeholderLeaf); storeBlockLeaf( CachedTextLeafSlot::Fallback, MarkedTextLeafSourceSignature( DisplayMathFallbackText(), st.displayMath.fallbackStyle, DisplayMathFallbackTextMinResizeWidth(st)), &block->fallbackLeaf); break; case PreparedBlockKind::Table: { storeBlockLeaf( CachedTextLeafSlot::Leaf, MarkedTextLeafSourceSignature( prepared.text, st.body, FlowTextMinResizeWidth(st.body)), &block->leaf); storeBlockLeaf( CachedTextLeafSlot::Placeholder, PlainTextLeafSourceSignature( prepared.editPlaceholderText, st.body, PlainTextMinResizeWidth(st.body)), &block->placeholderLeaf); const auto rowCount = std::min( int(prepared.tableRows.size()), int(block->tableRows.size())); for (auto rowIndex = 0; rowIndex != rowCount; ++rowIndex) { const auto cellCount = std::min( int(prepared.tableRows[rowIndex].cells.size()), int(block->tableRows[rowIndex].cells.size())); for (auto cellIndex = 0; cellIndex != cellCount; ++cellIndex) { const auto &preparedCell = prepared.tableRows[rowIndex].cells[cellIndex]; auto &cell = block->tableRows[rowIndex].cells[cellIndex]; const auto &textStyle = preparedCell.header ? st.table.headerStyle : st.table.bodyStyle; const auto minResizeWidth = TableCellTextMinResizeWidth( textStyle, st); storeTableCellLeaf( CachedTextLeafSlot::TableCellText, preparedCell, rowIndex, cellIndex, MarkedTextLeafSourceSignature( preparedCell.text, textStyle, minResizeWidth), &cell.leaf); storeTableCellLeaf( CachedTextLeafSlot::TableCellPlaceholder, preparedCell, rowIndex, cellIndex, PlainTextLeafSourceSignature( preparedCell.editPlaceholderText, textStyle, minResizeWidth), &cell.placeholderLeaf); } } } break; case PreparedBlockKind::Details: storeBlockLeaf( CachedTextLeafSlot::Leaf, MarkedTextLeafSourceSignature( prepared.text, st.details.summaryStyle, FlowTextMinResizeWidth(st.details.summaryStyle)), &block->leaf); storeBlockLeaf( CachedTextLeafSlot::Placeholder, PlainTextLeafSourceSignature( prepared.editPlaceholderText, st.details.summaryStyle, PlainTextMinResizeWidth(st.details.summaryStyle)), &block->placeholderLeaf); storeBlockLeaf( CachedTextLeafSlot::Action, PlainTextLeafSourceSignature( DetailsStateText(prepared.detailsOpen), st.details.summaryStyle, PlainTextMinResizeWidth(st.details.summaryStyle)), &block->actionLeaf); break; case PreparedBlockKind::Placeholder: storeBlockLeaf( CachedTextLeafSlot::Label, PlainTextLeafSourceSignature( prepared.placeholder.label, st.placeholder.labelStyle, PlainTextMinResizeWidth(st.placeholder.labelStyle)), &block->labelLeaf); storeBlockLeaf( CachedTextLeafSlot::Leaf, MarkedTextLeafSourceSignature( prepared.text, st.body, FlowTextMinResizeWidth(st.body)), &block->leaf); storeBlockLeaf( CachedTextLeafSlot::Placeholder, PlainTextLeafSourceSignature( prepared.editPlaceholderText, st.body, PlainTextMinResizeWidth(st.body)), &block->placeholderLeaf); break; case PreparedBlockKind::RelatedArticle: storeBlockLeaf( CachedTextLeafSlot::Label, PlainTextLeafSourceSignature( prepared.relatedArticle.title, st.relatedArticle.titleStyle, PlainTextMinResizeWidth(st.relatedArticle.titleStyle)), &block->labelLeaf); storeBlockLeaf( CachedTextLeafSlot::Subtitle, PlainTextLeafSourceSignature( prepared.relatedArticle.description, st.relatedArticle.subtitleStyle, PlainTextMinResizeWidth(st.relatedArticle.subtitleStyle)), &block->subtitleLeaf); storeBlockLeaf( CachedTextLeafSlot::Action, PlainTextLeafSourceSignature( prepared.relatedArticle.footer, st.relatedArticle.footerStyle, PlainTextMinResizeWidth(st.relatedArticle.footerStyle)), &block->actionLeaf); break; case PreparedBlockKind::EmbedPost: storeBlockLeaf( CachedTextLeafSlot::Label, PlainTextLeafSourceSignature( prepared.embedPost.author, st.embedPost.authorStyle, PlainTextMinResizeWidth(st.embedPost.authorStyle)), &block->labelLeaf); storeBlockLeaf( CachedTextLeafSlot::Subtitle, PlainTextLeafSourceSignature( prepared.embedPost.dateText, st.embedPost.dateStyle, PlainTextMinResizeWidth(st.embedPost.dateStyle)), &block->subtitleLeaf); break; case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: storeBlockLeaf( CachedTextLeafSlot::Leaf, MarkedTextLeafSourceSignature( prepared.text, st.body, FlowTextMinResizeWidth(st.body)), &block->leaf); storeBlockLeaf( CachedTextLeafSlot::Placeholder, PlainTextLeafSourceSignature( prepared.editPlaceholderText, st.body, PlainTextMinResizeWidth(st.body)), &block->placeholderLeaf); break; case PreparedBlockKind::Rule: case PreparedBlockKind::Quote: case PreparedBlockKind::List: case PreparedBlockKind::ListItem: break; } auto childPath = preparedPath; HarvestCachedTextLeafs(prepared.children, &block->children, st, pool, &childPath); } void HarvestCachedTextLeafs( const std::vector &preparedBlocks, std::vector *blocks, const style::Markdown &st, CachedTextLeafPool *pool, std::vector *preparedPath) { if (!blocks || !pool || !preparedPath) { return; } const auto count = std::min(int(preparedBlocks.size()), int(blocks->size())); for (auto i = 0; i != count; ++i) { preparedPath->push_back(i); HarvestCachedTextLeafs( preparedBlocks[i], &(*blocks)[i], st, pool, *preparedPath); preparedPath->pop_back(); } } void StoreRelatedArticleImageState( const LaidOutBlock &block, std::unordered_map *states) { if (block.thumbnailPhotoId) { (*states)[block.thumbnailPhotoId] = { .thumbnailImage = block.thumbnailImage, .previousThumbnailImage = block.previousThumbnailImage, .fullImage = block.fullImage, .previousFullImage = block.previousFullImage, }; } for (const auto &child : block.children) { StoreRelatedArticleImageState(child, states); } } void StoreRelatedArticleImageStates( const std::vector &blocks, std::unordered_map *states) { for (const auto &block : blocks) { StoreRelatedArticleImageState(block, states); } } void RestoreRelatedArticleImageState( LaidOutBlock *block, const std::unordered_map &states) { if (block->thumbnailPhotoId) { if (const auto i = states.find(block->thumbnailPhotoId); i != end(states)) { block->thumbnailImage = i->second.thumbnailImage; block->previousThumbnailImage = i->second.previousThumbnailImage; block->fullImage = i->second.fullImage; block->previousFullImage = i->second.previousFullImage; block->subscribedThumbnailImage.reset(); block->thumbnailRequestSize = QSize(); block->subscribedFullImage.reset(); block->fullRequestSize = QSize(); } } for (auto &child : block->children) { RestoreRelatedArticleImageState(&child, states); } } void RestoreRelatedArticleImageStates( std::vector *blocks, const std::unordered_map &states) { for (auto &block : *blocks) { RestoreRelatedArticleImageState(&block, states); } } [[nodiscard]] bool IsDisplayMathSegment(const SelectableSegment &segment) { return (segment.kind == SelectableSegmentKind::DisplayMath); } [[nodiscard]] bool IsEditableSegment(const SelectableSegment &segment) { return segment.isTextLeaf() || IsDisplayMathSegment(segment); } [[nodiscard]] QColor MarkBgColorForStyle(const style::Markdown &st) { auto result = st.textPalette.markBg->c; result.setAlphaF(result.alphaF() * std::clamp( st.markBgOpacity, 0., 1.)); return result; } template [[nodiscard]] int CompareValues(const T &a, const T &b) { return (a < b) ? -1 : (b < a) ? 1 : 0; } [[nodiscard]] int ComparePreparedEditBlockContainerStep( const PreparedEditBlockContainerStep &a, const PreparedEditBlockContainerStep &b) { if (const auto result = CompareValues(int(a.kind), int(b.kind)); result != 0) { return result; } if (const auto result = CompareValues(a.blockIndex, b.blockIndex); result != 0) { return result; } return CompareValues(a.listItemIndex, b.listItemIndex); } [[nodiscard]] int ComparePreparedEditBlockContainerPath( const PreparedEditBlockContainerPath &a, const PreparedEditBlockContainerPath &b) { const auto count = std::min(a.steps.size(), b.steps.size()); for (auto i = size_t(0); i != count; ++i) { if (const auto result = ComparePreparedEditBlockContainerStep( a.steps[i], b.steps[i]); result != 0) { return result; } } return CompareValues(a.steps.size(), b.steps.size()); } [[nodiscard]] int ComparePreparedEditBlockPath( const PreparedEditBlockPath &a, const PreparedEditBlockPath &b) { if (const auto result = ComparePreparedEditBlockContainerPath( a.container, b.container); result != 0) { return result; } return CompareValues(a.index, b.index); } [[nodiscard]] int ComparePreparedEditListItemSource( const PreparedEditListItemSource &a, const PreparedEditListItemSource &b) { if (const auto result = ComparePreparedEditBlockPath(a.block, b.block); result != 0) { return result; } return CompareValues(a.listItemIndex, b.listItemIndex); } struct PreparedEditListItemSourceLess { [[nodiscard]] bool operator()( const PreparedEditListItemSource &a, const PreparedEditListItemSource &b) const { return ComparePreparedEditListItemSource(a, b) < 0; } }; using TaskMarkerRippleRuntimeMap = std::map< PreparedEditListItemSource, std::shared_ptr, PreparedEditListItemSourceLess>; using TaskMarkerSourceSet = std::set< PreparedEditListItemSource, PreparedEditListItemSourceLess>; void CollectPlaceholderIds( const std::vector &blocks, std::unordered_set *result) { if (!result) { return; } for (const auto &block : blocks) { if (block.placeholderId) { result->emplace(block.placeholderId.value); } CollectPlaceholderIds(block.children, result); } } [[nodiscard]] LaidOutBlock *FindPlaceholderBlock( std::vector *blocks, PreparedPlaceholderBlockId id) { if (!blocks || !id) { return nullptr; } for (auto &block : *blocks) { if (block.placeholderId.value == id.value) { return █ } if (const auto child = FindPlaceholderBlock(&block.children, id)) { return child; } } return nullptr; } void CollectTaskMarkerSources( const std::vector &blocks, TaskMarkerSourceSet *result) { if (!result) { return; } for (const auto &block : blocks) { if (block.taskState != TaskState::None && block.editListItem) { result->emplace(*block.editListItem); } CollectTaskMarkerSources(block.children, result); } } [[nodiscard]] LaidOutBlock *FindListItemBlock( std::vector *blocks, const PreparedEditListItemSource &source) { if (!blocks) { return nullptr; } for (auto &block : *blocks) { if (block.kind == PreparedBlockKind::ListItem && block.editListItem && (*block.editListItem == source)) { return █ } if (const auto child = FindListItemBlock(&block.children, source)) { return child; } } return nullptr; } [[nodiscard]] const LaidOutBlock *FindListItemBlock( const std::vector &blocks, const PreparedEditListItemSource &source) { for (const auto &block : blocks) { if (block.kind == PreparedBlockKind::ListItem && block.editListItem && (*block.editListItem == source)) { return █ } if (const auto child = FindListItemBlock(block.children, source)) { return child; } } return nullptr; } [[nodiscard]] QRect TaskMarkerRippleRect( const LaidOutBlock &block, const style::Markdown &st) { if (block.markerRect.isEmpty()) { return {}; } const auto size = (block.taskMarkerRippleRuntime && !block.taskMarkerRippleRuntime->rippleSize.isEmpty()) ? block.taskMarkerRippleRuntime->rippleSize : (block.markerRect.size() + QSize( st.list.taskCheck.rippleAreaPadding * 2, st.list.taskCheck.rippleAreaPadding * 2)); return QRect( block.markerRect.topLeft() + st::defaultCheckbox.rippleAreaPosition, size); } [[nodiscard]] const LaidOutBlock *FindPlaceholderBlock( const std::vector &blocks, PreparedPlaceholderBlockId id) { if (!id) { return nullptr; } for (const auto &block : blocks) { if (block.placeholderId.value == id.value) { return █ } if (const auto child = FindPlaceholderBlock(block.children, id)) { return child; } } return nullptr; } void AppendRevealLine( std::vector *lines, int left, int width, int bottom, int baseline, bool rtl) { if (width < 0) { return; } const auto previousBottom = lines->empty() ? std::numeric_limits::lowest() : lines->back().bottom; if (bottom <= previousBottom) { return; } lines->push_back({ .left = left, .width = width, .bottom = bottom, .rtl = rtl, .baseline = baseline, }); } void AppendGenericRevealBand( std::vector *lines, QRect rect) { if (rect.isEmpty()) { return; } const auto bottom = rect.y() + rect.height(); AppendRevealLine( lines, rect.x(), rect.width(), bottom, bottom, false); } [[nodiscard]] QRect ClipRectToViewport(QRect rect, QRect viewport); void AppendTextRevealLines( std::vector *lines, const Ui::Text::String &leaf, QRect textRect, QRect visibleRect, int textWidth) { if (textRect.isEmpty() || visibleRect.isEmpty() || (textWidth <= 0)) { return; } const auto geometry = leaf.countLinesGeometry(textWidth, true); const auto viewportLeft = visibleRect.x(); const auto viewportRight = visibleRect.x() + visibleRect.width(); for (const auto &line : geometry) { const auto left = textRect.x() + line.left; const auto right = left + line.width; const auto visibleLeft = std::clamp( left, viewportLeft, viewportRight); const auto visibleRight = std::clamp( right, viewportLeft, viewportRight); AppendRevealLine( lines, visibleLeft, visibleRight - visibleLeft, textRect.y() + line.bottom, textRect.y() + line.baseline, line.rtl); } } void AppendBlocksRevealLines( const std::vector &blocks, const style::Markdown &st, std::vector *lines); void AppendTableRowRevealLines( std::vector *lines, const LaidOutBlock &block, const style::Markdown &st) { if (block.visibleTableRect.isEmpty()) { return; } const auto border = std::max(block.tableBordered ? st.table.border : 0, 0); const auto tableBottom = block.visibleTableRect.y() + block.visibleTableRect.height(); for (const auto &row : block.tableRows) { if (row.outer.height() <= 0) { continue; } const auto bottom = std::min( row.outer.y() + row.outer.height() + border, tableBottom); AppendRevealLine( lines, block.visibleTableRect.x(), block.visibleTableRect.width(), bottom, bottom, false); } } void AppendTableRevealLines( std::vector *lines, const LaidOutBlock &block, const style::Markdown &st) { AppendTextRevealLines( lines, block.leaf, block.textRect, ClipRectToViewport(block.textRect, block.outer), block.textWidth); AppendTableRowRevealLines(lines, block, st); } void AppendMediaRevealLines( std::vector *lines, const LaidOutBlock &block) { AppendGenericRevealBand(lines, block.visibleMediaRect); AppendTextRevealLines( lines, block.leaf, block.textRect, ClipRectToViewport(block.textRect, block.outer), block.textWidth); } void AppendEmbedPostRevealLines( std::vector *lines, const LaidOutBlock &block, const style::Markdown &st) { if (!block.headerRect.isEmpty()) { const auto bottom = block.headerRect.y() + block.headerRect.height(); AppendRevealLine( lines, block.mediaRect.x(), block.mediaRect.width(), bottom, bottom, false); } else if (block.children.empty()) { AppendGenericRevealBand(lines, block.mediaRect); } AppendBlocksRevealLines(block.children, st, lines); AppendTextRevealLines( lines, block.leaf, block.textRect, ClipRectToViewport(block.textRect, block.outer), block.textWidth); } void AppendDetailsRevealLines( std::vector *lines, const LaidOutBlock &block, const style::Markdown &st) { AppendTextRevealLines( lines, block.leaf, block.textRect, ClipRectToViewport(block.textRect, block.headerRect), block.textWidth); AppendBlocksRevealLines(block.children, st, lines); } void AppendBlockRevealLines( std::vector *lines, const LaidOutBlock &block, const style::Markdown &st) { switch (block.kind) { case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::CodeBlock: AppendTextRevealLines( lines, block.leaf, block.textRect, (block.kind == PreparedBlockKind::CodeBlock) ? ClipRectToViewport(block.textRect, block.contentRect) : ClipRectToViewport(block.textRect, block.outer), block.textWidth); break; case PreparedBlockKind::Rule: AppendGenericRevealBand(lines, block.outer); break; case PreparedBlockKind::List: case PreparedBlockKind::ListItem: case PreparedBlockKind::Quote: AppendBlocksRevealLines(block.children, st, lines); break; case PreparedBlockKind::DisplayMath: AppendGenericRevealBand(lines, block.visibleFormulaRect); break; case PreparedBlockKind::Table: AppendTableRevealLines(lines, block, st); break; case PreparedBlockKind::Details: AppendDetailsRevealLines(lines, block, st); break; case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::Placeholder: AppendMediaRevealLines(lines, block); break; case PreparedBlockKind::RelatedArticle: AppendGenericRevealBand(lines, block.visibleMediaRect); break; case PreparedBlockKind::EmbedPost: AppendEmbedPostRevealLines(lines, block, st); break; } } void AppendBlocksRevealLines( const std::vector &blocks, const style::Markdown &st, std::vector *lines) { for (const auto &block : blocks) { AppendBlockRevealLines(lines, block, st); } } [[nodiscard]] std::vector CollectRevealLines( const std::vector &blocks, const style::Markdown &st) { auto result = std::vector(); AppendBlocksRevealLines(blocks, st, &result); return result; } [[nodiscard]] PendingHighlightKey PendingHighlightKeyForBlock( const LaidOutBlock &block) { return { .text = CodeBlockDisplayText(block.copyText), .language = block.codeLanguage, }; } [[nodiscard]] Ui::Text::StateResult TextStateAtLeaf( const Ui::Text::String &leaf, QRect rect, int width, QPoint point, Ui::Text::StateRequest::Flags flags, style::align align = style::al_left, bool clampToRect = false) { if (rect.isEmpty()) { return {}; } if (!rect.contains(point)) { if (!clampToRect) { return {}; } point.setX(std::clamp(point.x(), rect.left(), rect.right())); point.setY(std::clamp(point.y(), rect.top(), rect.bottom())); } auto request = Ui::Text::StateRequest(); request.align = align; request.flags = flags | Ui::Text::StateRequest::Flag::BreakEverywhere; const auto availableWidth = std::max(width, 1); return leaf.getState( point - rect.topLeft(), TextGeometry(availableWidth), request); } [[nodiscard]] SegmentSpan FullSegmentSpan( const std::vector &segments) { return { 0, int(segments.size()) }; } void RebuildVisibleSegmentLookup( const std::vector &segments, std::vector *tops, std::vector *bottoms) { if (!tops || !bottoms) { return; } tops->clear(); bottoms->clear(); tops->reserve(segments.size()); bottoms->reserve(segments.size()); auto runningBottom = std::numeric_limits::lowest(); for (const auto &segment : segments) { tops->push_back(segment.outerRect.top()); runningBottom = std::max(runningBottom, segment.outerRect.bottom()); bottoms->push_back(runningBottom); } } [[nodiscard]] SegmentSpan LookupVisibleSegmentSpan( const std::vector &tops, const std::vector &bottoms, LogicalVisibleRange range) { if (tops.empty() || bottoms.empty() || (range.bottom <= range.top)) { return {}; } const auto from = int(std::lower_bound( bottoms.begin(), bottoms.end(), range.top) - bottoms.begin()); if (from >= int(tops.size())) { return {}; } const auto till = int(std::upper_bound( tops.begin() + from, tops.end(), range.bottom - 1) - tops.begin()); return (from < till) ? SegmentSpan{ from, till } : SegmentSpan(); } [[nodiscard]] std::optional PreparedLinkForMediaActivation( const MediaActivation &activation) { if (activation.kind != MediaActivationKind::ExternalUrl || activation.url.isEmpty()) { return std::nullopt; } if (const auto prepared = ClassifiedLink(0, activation.url, nullptr); prepared.kind == PreparedLinkKind::External) { return prepared; } return PreparedLink{ .kind = PreparedLinkKind::External, .target = activation.url, .copyText = UrlClickHandler::EncodeForOpening(activation.url), .entityType = EntityType::Url, .shown = EntityLinkShown::Full, }; } [[nodiscard]] std::optional PreparedLinkForDetailsBlock( const SelectableSegment &segment) { if (!segment.block || segment.block->kind != PreparedBlockKind::Details || segment.block->anchorId.isEmpty()) { return std::nullopt; } return PreparedLink{ .kind = PreparedLinkKind::ToggleDetails, .target = segment.block->anchorId, }; } [[nodiscard]] int ComputeScrollTo( int toFrom, int toTill, int toMin, int toMax, int current, int size) { if (toFrom < toMin) { toFrom = toMin; } else if (toFrom > toMax) { toFrom = toMax; } const auto exact = (toTill < 0); const auto curBottom = current + size; auto scToFrom = toFrom; if (!exact && toFrom >= current) { if (toTill < toFrom) { toTill = toFrom; } if (toTill <= curBottom) { return current; } scToFrom = toTill - size; if (scToFrom > toFrom) { scToFrom = toFrom; } if (scToFrom == current) { return current; } } else { scToFrom = toFrom; } return scToFrom; } [[nodiscard]] QRect LogicalTextRectForSegment(const SelectableSegment &segment) { if (segment.cell) { return segment.cell->textRect; } else if (!segment.block) { return segment.textRect; } else if (segment.leaf == &segment.block->leaf) { return segment.block->textRect; } else if (segment.leaf == &segment.block->labelLeaf) { return segment.block->labelRect; } else if (segment.leaf == &segment.block->subtitleLeaf) { return segment.block->subtitleRect; } else if (segment.leaf == &segment.block->actionLeaf) { return segment.block->actionRect; } return segment.textRect; } [[nodiscard]] MarkdownArticleHitTestResult HitSegmentBoundary( const SelectableSegment &segment, int offset) { auto result = MarkdownArticleHitTestResult(); result.segmentIndex = segment.index; result.forcedOffset = std::clamp(offset, 0, SegmentLength(segment)); result.state.uponSymbol = true; result.state.afterSymbol = (result.forcedOffset > 0); return result; } [[nodiscard]] MarkdownArticleHitTestResult HitTextSegment( const SelectableSegment &segment, QPoint point, Ui::Text::StateRequest::Flags flags) { if (!segment.isTextLeaf() || !segment.outerRect.contains(point)) { return {}; } const auto insideText = segment.textRect.contains(point); if (!insideText && !(flags & Ui::Text::StateRequest::Flag::LookupSymbol)) { return {}; } auto result = MarkdownArticleHitTestResult(); result.segmentIndex = segment.index; const auto logicalRect = LogicalTextRectForSegment(segment); result.state = TextStateAtLeaf( *segment.leaf, logicalRect, segment.textWidth, point, flags, segment.align, !insideText); if (!insideText) { result.state.link = nullptr; } result.preparedLink = ExtractPreparedLink(result.state.link); if (!result.preparedLink && (flags & Ui::Text::StateRequest::Flag::LookupLink)) { if (const auto prepared = PreparedLinkForDetailsBlock(segment)) { result.preparedLink = prepared; if (!insideText) { result.state.link = CreatePreparedLinkHandler(*prepared); } } } result.direct = true; return result; } [[nodiscard]] MarkdownArticleHitTestResult HitCodeBlockHeader( const SelectableSegment &segment, QPoint point, Ui::Text::StateRequest::Flags flags) { if (segment.kind != SelectableSegmentKind::CodeBlock || !segment.block || !(flags & Ui::Text::StateRequest::Flag::LookupSymbol) || !segment.block->headerRect.contains(point)) { return {}; } auto result = MarkdownArticleHitTestResult(); result.segmentIndex = segment.index; result.forcedOffset = 0; result.state.uponSymbol = true; result.direct = true; result.codeHeaderCopy = true; return result; } [[nodiscard]] MarkdownArticleHitTestResult HitBlockSegment( const SelectableSegment &segment, QPoint point, Ui::Text::StateRequest::Flags flags) { if (segment.isTextLeaf() || !(flags & Ui::Text::StateRequest::Flag::LookupSymbol) || !segment.outerRect.contains(point)) { return {}; } const auto after = (point.y() > segment.outerRect.center().y()) || ((point.y() == segment.outerRect.center().y()) && (point.x() >= segment.outerRect.center().x())); auto result = HitSegmentBoundary( segment, after ? SegmentLength(segment) : 0); const auto applyActivation = [&](const MediaActivation &activation) { result.mediaActivation = activation; if (const auto prepared = PreparedLinkForMediaActivation(activation)) { result.preparedLink = prepared; result.state.link = CreatePreparedLinkHandler(*prepared); } }; if (segment.block) { if (segment.block->kind == PreparedBlockKind::RelatedArticle && segment.block->preparedLink) { result.preparedLink = segment.block->preparedLink; result.state.link = segment.block->preparedLinkHandler; result.mediaActivation = {}; } else if (segment.block->mediaBlock) { if (const auto link = segment.block->mediaBlock->linkAt(point)) { result.state.link = link; result.preparedLink = std::nullopt; result.mediaActivation = {}; } else { applyActivation(segment.block->mediaBlock->activationAt(point)); } } else { applyActivation(segment.block->activation); if (result.mediaActivation.kind == MediaActivationKind::Embed && segment.block->placeholderRuntime) { result.state.link = segment.block->placeholderRuntime->clickHandler; result.placeholderLocalPoint = point - segment.block->mediaRect.topLeft(); } } } result.direct = true; return result; } [[nodiscard]] MarkdownArticleHitTestResult HitSegmentFallback( const std::vector &segments, SegmentSpan span, QPoint point) { if (segments.empty() || span.empty()) { return {}; } for (auto i = span.from; i != span.till; ++i) { const auto &segment = segments[i]; const auto &rect = segment.outerRect; if (point.y() < rect.top()) { return HitSegmentBoundary(segment, 0); } if (point.y() <= rect.bottom()) { if (point.x() < rect.left()) { return HitSegmentBoundary(segment, 0); } else if (point.x() > rect.right()) { return HitSegmentBoundary( segment, SegmentLength(segment)); } } } return HitSegmentBoundary( segments[span.till - 1], SegmentLength(segments[span.till - 1])); } [[nodiscard]] bool ContainsPoint(QRect rect, QPoint point) { return !rect.isEmpty() && rect.contains(point); } struct ActiveHorizontalScrollOwnerState { QRect viewport; int shift = 0; }; [[nodiscard]] QRect TranslateRect(QRect rect, int shift) { return rect.isEmpty() ? rect : rect.translated(shift, 0); } [[nodiscard]] QRect ClipRectToViewport(QRect rect, QRect viewport) { if (rect.isEmpty() || viewport.isEmpty()) { return QRect(); } const auto left = std::max(rect.x(), viewport.x()); const auto right = std::min( rect.x() + rect.width(), viewport.x() + viewport.width()); return QRect( left, rect.y(), std::max(right - left, 0), rect.height()); } [[nodiscard]] QPoint TranslatePoint(QPoint point, int shift) { return point + QPoint(shift, 0); } [[nodiscard]] QPoint ClampPointToViewport(QPoint point, QRect viewport) { if (viewport.isEmpty()) { return point; } const auto right = viewport.x() + std::max(viewport.width() - 1, 0); point.setX(std::clamp(point.x(), viewport.x(), right)); return point; } [[nodiscard]] QRect BaseVisibleTableRect(const LaidOutBlock &block) { if (!block.scrollViewportRect.isEmpty()) { return block.scrollViewportRect; } else if (block.logicalGeometry.tableRect.isEmpty()) { return QRect(); } return QRect( block.logicalGeometry.tableRect.x(), block.logicalGeometry.tableRect.y(), std::min( block.logicalGeometry.tableRect.width(), std::max(block.logicalGeometry.outer.width(), 1)), block.logicalGeometry.tableRect.height()); } void RestoreLogicalBlockGeometry(LaidOutBlock *block) { block->outer = block->logicalGeometry.outer; block->headerRect = block->logicalGeometry.headerRect; block->bodyRect = block->logicalGeometry.bodyRect; block->iconRect = block->logicalGeometry.iconRect; block->textRect = block->logicalGeometry.textRect; block->labelRect = block->logicalGeometry.labelRect; block->subtitleRect = block->logicalGeometry.subtitleRect; block->actionRect = block->logicalGeometry.actionRect; block->markerRect = block->logicalGeometry.markerRect; block->contentRect = block->logicalGeometry.contentRect; block->formulaRect = block->logicalGeometry.formulaRect; block->tableRect = block->logicalGeometry.tableRect; block->mediaRect = block->logicalGeometry.mediaRect; block->thumbnailRect = block->logicalGeometry.thumbnailRect; block->markerCenter = block->logicalGeometry.markerCenter; block->visibleFormulaRect = block->formulaRect.intersected( block->contentRect); block->visibleTableRect = BaseVisibleTableRect(*block); block->tableScrollbarTrackRect = block->scrollScrollbarTrackRect; block->tableScrollbarThumbRect = QRect(); block->scrollScrollbarThumbRect = QRect(); block->visibleMediaRect = block->mediaRect; block->insideHorizontalScroll = false; block->horizontalScrollAncestorShift = 0; for (auto &row : block->tableRows) { row.outer = row.logicalOuter; for (auto &cell : row.cells) { cell.outer = cell.logicalOuter; cell.textRect = cell.logicalTextRect; } } } [[nodiscard]] bool ScrollOwnerMovesOwnContent(PreparedBlockKind kind) { switch (kind) { case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::CodeBlock: case PreparedBlockKind::DisplayMath: case PreparedBlockKind::Table: return true; case PreparedBlockKind::Rule: case PreparedBlockKind::List: case PreparedBlockKind::ListItem: case PreparedBlockKind::Quote: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::EmbedPost: case PreparedBlockKind::Placeholder: case PreparedBlockKind::Details: return false; } return false; } [[nodiscard]] bool ScrollOwnerMovesChildren(PreparedBlockKind kind) { switch (kind) { case PreparedBlockKind::List: case PreparedBlockKind::ListItem: case PreparedBlockKind::Quote: case PreparedBlockKind::EmbedPost: case PreparedBlockKind::Details: return true; case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::CodeBlock: case PreparedBlockKind::Rule: case PreparedBlockKind::DisplayMath: case PreparedBlockKind::Table: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::Placeholder: return false; } return false; } void ApplyTranslatedDescendantGeometry( LaidOutBlock *block, const ActiveHorizontalScrollOwnerState &state) { block->insideHorizontalScroll = true; block->horizontalScrollAncestorShift = state.shift; block->outer = ClipRectToViewport( TranslateRect(block->outer, state.shift), state.viewport); block->headerRect = ClipRectToViewport( TranslateRect(block->headerRect, state.shift), state.viewport); block->bodyRect = ClipRectToViewport( TranslateRect(block->bodyRect, state.shift), state.viewport); block->iconRect = ClipRectToViewport( TranslateRect(block->iconRect, state.shift), state.viewport); block->contentRect = ClipRectToViewport( TranslateRect(block->contentRect, state.shift), state.viewport); block->textRect = TranslateRect(block->textRect, state.shift); block->labelRect = TranslateRect(block->labelRect, state.shift); block->subtitleRect = TranslateRect(block->subtitleRect, state.shift); block->actionRect = TranslateRect(block->actionRect, state.shift); block->markerRect = ClipRectToViewport( TranslateRect(block->markerRect, state.shift), state.viewport); block->formulaRect = TranslateRect(block->formulaRect, state.shift); block->tableRect = TranslateRect(block->tableRect, state.shift); block->mediaRect = TranslateRect(block->mediaRect, state.shift); block->thumbnailRect = TranslateRect(block->thumbnailRect, state.shift); block->markerCenter = ClampPointToViewport( TranslatePoint(block->markerCenter, state.shift), state.viewport); block->visibleFormulaRect = ClipRectToViewport( block->formulaRect, state.viewport); block->visibleTableRect = ClipRectToViewport( TranslateRect(BaseVisibleTableRect(*block), state.shift), state.viewport); block->visibleMediaRect = ClipRectToViewport( block->mediaRect, state.viewport); for (auto &row : block->tableRows) { row.outer = ClipRectToViewport( TranslateRect(row.logicalOuter, state.shift), state.viewport); for (auto &cell : row.cells) { cell.outer = ClipRectToViewport( TranslateRect(cell.logicalOuter, state.shift), state.viewport); cell.textRect = TranslateRect(cell.logicalTextRect, state.shift); } } } void ApplyOwnerContentGeometry( LaidOutBlock *block, const ActiveHorizontalScrollOwnerState &state) { switch (block->kind) { case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::CodeBlock: block->textRect = TranslateRect(block->logicalGeometry.textRect, state.shift); break; case PreparedBlockKind::DisplayMath: block->formulaRect = TranslateRect( block->logicalGeometry.formulaRect, state.shift); block->textRect = TranslateRect(block->logicalGeometry.textRect, state.shift); block->visibleFormulaRect = ClipRectToViewport( block->formulaRect, state.viewport); break; case PreparedBlockKind::Table: block->visibleTableRect = block->scrollViewportRect; block->tableScrollbarTrackRect = block->scrollScrollbarTrackRect; for (auto &row : block->tableRows) { row.outer = ClipRectToViewport( TranslateRect(row.logicalOuter, state.shift), block->visibleTableRect); for (auto &cell : row.cells) { cell.outer = ClipRectToViewport( TranslateRect(cell.logicalOuter, state.shift), block->visibleTableRect); cell.textRect = TranslateRect( cell.logicalTextRect, state.shift); } } break; case PreparedBlockKind::Rule: case PreparedBlockKind::List: case PreparedBlockKind::ListItem: case PreparedBlockKind::Quote: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::EmbedPost: case PreparedBlockKind::Placeholder: case PreparedBlockKind::Details: break; } } [[nodiscard]] bool ValidBlockPath(const PreparedEditBlockPath &path) { return (path.index >= 0); } [[nodiscard]] PreparedEditBlockSource EditBlockSourceFromPath( PreparedEditBlockPath path) { return { .path = std::move(path) }; } [[nodiscard]] PreparedEditTableRowSource EditTableRowSourceFromCell( const PreparedEditTableCellSource &source) { return { .block = source.block, .tableRowIndex = source.tableRowIndex, }; } [[nodiscard]] PreparedEditHit WithLeaf( PreparedEditHit hit, const std::optional &leaf) { if (leaf) { hit.leaf = *leaf; } return hit; } [[nodiscard]] PreparedEditHit EditHitFromBlockSource( const PreparedEditBlockSource &source, const std::optional &leaf = std::nullopt) { if (!ValidBlockPath(source.path)) { return {}; } auto result = PreparedEditHit(); result.kind = PreparedEditHitKind::Block; result.block = source; return WithLeaf(std::move(result), leaf); } [[nodiscard]] PreparedEditHit EditHitFromListItemSource( const PreparedEditListItemSource &source, const std::optional &leaf = std::nullopt) { if (!ValidBlockPath(source.block) || source.listItemIndex < 0) { return {}; } auto result = PreparedEditHit(); result.kind = PreparedEditHitKind::ListItem; result.block = EditBlockSourceFromPath(source.block); result.listItem = source; return WithLeaf(std::move(result), leaf); } [[nodiscard]] PreparedEditHit EditHitFromTableRowSource( const PreparedEditTableRowSource &source, const std::optional &leaf = std::nullopt) { if (!ValidBlockPath(source.block) || source.tableRowIndex < 0) { return {}; } auto result = PreparedEditHit(); result.kind = PreparedEditHitKind::TableRow; result.block = EditBlockSourceFromPath(source.block); result.tableRow = source; return WithLeaf(std::move(result), leaf); } [[nodiscard]] PreparedEditHit EditHitFromTableCellSource( const PreparedEditTableCellSource &source, const std::optional &leaf = std::nullopt) { if (!ValidBlockPath(source.block) || source.tableRowIndex < 0 || source.tableCellIndex < 0) { return {}; } auto result = PreparedEditHit(); result.kind = PreparedEditHitKind::TableCell; result.block = EditBlockSourceFromPath(source.block); result.tableRow = EditTableRowSourceFromCell(source); result.tableCell = source; return WithLeaf(std::move(result), leaf); } [[nodiscard]] PreparedEditHit EditHitFromLeafSource( const PreparedEditLeafSource &source, bool preferLeaf) { if (!ValidBlockPath(source.block)) { return {}; } switch (source.kind) { case PreparedEditLeafKind::ListItemText: return EditHitFromListItemSource( PreparedEditListItemSource{ .block = source.block, .listItemIndex = source.listItemIndex, }, source); case PreparedEditLeafKind::TableCellText: return EditHitFromTableCellSource( PreparedEditTableCellSource{ .block = source.block, .tableRowIndex = source.tableRowIndex, .tableCellIndex = source.tableCellIndex, }, source); case PreparedEditLeafKind::BlockText: case PreparedEditLeafKind::BlockCaption: case PreparedEditLeafKind::MathFormula: { auto result = PreparedEditHit(); result.kind = preferLeaf ? PreparedEditHitKind::Leaf : PreparedEditHitKind::Block; result.block = EditBlockSourceFromPath(source.block); result.leaf = source; return result; } break; } return {}; } [[nodiscard]] PreparedEditHit EditFallbackHitForBlock( const LaidOutBlock &block); [[nodiscard]] PreparedEditHit EditHitForBlock( const LaidOutBlock &block, QPoint point); [[nodiscard]] PreparedEditBlockContainerPath ListItemChildContainer( const PreparedEditListItemSource &source) { auto result = source.block.container; result.steps.push_back({ .kind = PreparedEditBlockContainerKind::ListItemChildren, .blockIndex = source.block.index, .listItemIndex = source.listItemIndex, }); return result; } [[nodiscard]] bool HitHasRealBlockInContainer( const PreparedEditHit &hit, const PreparedEditBlockContainerPath &container) { return hit.block && (hit.block->path.container.steps.size() >= container.steps.size()) && std::equal( container.steps.begin(), container.steps.end(), hit.block->path.container.steps.begin()); } [[nodiscard]] PreparedEditHit EditHitForBlocks( const std::vector &blocks, QPoint point) { auto fallback = PreparedEditHit(); auto fallbackDistance = std::numeric_limits::max(); for (const auto &block : blocks) { if (ContainsPoint(block.outer, point)) { return EditHitForBlock(block, point); } const auto candidate = EditFallbackHitForBlock(block); if (!candidate.valid()) { continue; } const auto distance = (point.y() < block.outer.top()) ? (block.outer.top() - point.y()) : (point.y() > block.outer.bottom()) ? (point.y() - block.outer.bottom()) : 0; if (distance < fallbackDistance) { fallback = candidate; fallbackDistance = distance; } } return fallback; } void CollectMediaBlockGeometries( std::vector *out, const std::vector &blocks) { for (const auto &block : blocks) { const auto media = (block.kind == PreparedBlockKind::Photo) || (block.kind == PreparedBlockKind::Video) || (block.kind == PreparedBlockKind::Audio) || (block.kind == PreparedBlockKind::Map) || (block.kind == PreparedBlockKind::GroupedMedia); if (media && block.editBlock) { const auto grouped = (block.kind == PreparedBlockKind::GroupedMedia); auto itemRects = std::vector(); auto activeItemIndex = -1; if (grouped && block.mediaBlock) { itemRects = block.mediaBlock->itemRects(); activeItemIndex = block.mediaBlock->activeItemIndex(); } out->push_back({ .block = *block.editBlock, .mediaRect = block.mediaRect, .visibleMediaRect = block.visibleMediaRect, .grouped = grouped, .itemRects = std::move(itemRects), .activeItemIndex = activeItemIndex, }); } if (!block.children.empty()) { CollectMediaBlockGeometries(out, block.children); } } } [[nodiscard]] PreparedEditHit EditFallbackHitForBlock( const LaidOutBlock &block) { if (block.editListItem) { return EditHitFromListItemSource(*block.editListItem); } else if (block.editBlock) { return EditHitFromBlockSource(*block.editBlock); } else if (block.editLeaf) { return EditHitFromLeafSource(*block.editLeaf, false); } return {}; } [[nodiscard]] MarkdownArticleEditControlHit EditControlHitForBlocks( const std::vector &blocks, QPoint point); [[nodiscard]] QRect VisibleTextRectForBlock(const LaidOutBlock &block) { switch (block.kind) { case PreparedBlockKind::CodeBlock: return ClipRectToViewport(block.textRect, block.contentRect); case PreparedBlockKind::Details: return ClipRectToViewport(block.textRect, block.headerRect); case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::Rule: case PreparedBlockKind::DisplayMath: case PreparedBlockKind::Table: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::EmbedPost: case PreparedBlockKind::Placeholder: case PreparedBlockKind::List: case PreparedBlockKind::ListItem: case PreparedBlockKind::Quote: return ClipRectToViewport(block.textRect, block.outer); } return ClipRectToViewport(block.textRect, block.outer); } [[nodiscard]] PreparedEditHit EditHitForTableCell( const LaidOutTableCell &cell, QPoint point) { auto leaf = ContainsPoint(ClipRectToViewport(cell.textRect, cell.outer), point) ? cell.editLeaf : std::optional(); if (cell.editCell) { return EditHitFromTableCellSource(*cell.editCell, leaf); } else if (leaf) { return EditHitFromLeafSource(*leaf, false); } return {}; } [[nodiscard]] PreparedEditHit EditHitForTableBlock( const LaidOutBlock &block, QPoint point) { if (ContainsPoint(VisibleTextRectForBlock(block), point) && block.editLeaf) { if (block.editBlock) { return EditHitFromBlockSource(*block.editBlock, block.editLeaf); } return EditHitFromLeafSource(*block.editLeaf, false); } for (const auto &row : block.tableRows) { for (const auto &cell : row.cells) { if (ContainsPoint(cell.outer, point)) { if (const auto result = EditHitForTableCell(cell, point); result.valid()) { return result; } } } } for (const auto &row : block.tableRows) { if (ContainsPoint(row.outer, point)) { if (row.editRow) { return EditHitFromTableRowSource(*row.editRow); } } } return EditFallbackHitForBlock(block); } [[nodiscard]] PreparedEditHit EditHitForListBlock( const LaidOutBlock &block, QPoint point) { if (!block.children.empty()) { if (const auto result = EditHitForBlocks(block.children, point); result.valid()) { return result; } } return EditFallbackHitForBlock(block); } [[nodiscard]] PreparedEditHit EditHitForListItemBlock( const LaidOutBlock &block, QPoint point) { const auto listItemHit = EditFallbackHitForBlock(block); if (ContainsPoint(block.contentRect, point) && !block.children.empty()) { if (const auto childHit = EditHitForBlocks(block.children, point); childHit.valid()) { if (block.editListItem && HitHasRealBlockInContainer( childHit, ListItemChildContainer(*block.editListItem))) { return childHit; } return childHit.leaf ? WithLeaf(listItemHit, childHit.leaf) : listItemHit; } } return listItemHit; } [[nodiscard]] PreparedEditHit EditHitForQuoteBlock( const LaidOutBlock &block, QPoint point) { if (ContainsPoint(block.contentRect, point) && !block.children.empty()) { if (const auto childHit = EditHitForBlocks(block.children, point); childHit.valid()) { return childHit; } } return EditFallbackHitForBlock(block); } [[nodiscard]] PreparedEditHit EditHitForDetailsBlock( const LaidOutBlock &block, QPoint point) { if (ContainsPoint(block.headerRect, point)) { if (ContainsPoint(VisibleTextRectForBlock(block), point) && block.editLeaf) { if (block.editBlock) { return EditHitFromBlockSource(*block.editBlock, block.editLeaf); } return EditHitFromLeafSource(*block.editLeaf, false); } return EditFallbackHitForBlock(block); } if (ContainsPoint(block.contentRect, point) && !block.children.empty()) { if (const auto childHit = EditHitForBlocks(block.children, point); childHit.valid()) { return childHit; } } return EditFallbackHitForBlock(block); } [[nodiscard]] MarkdownArticleEditControlHit EditControlHitForListItemBlock( const LaidOutBlock &block, QPoint point) { if (block.taskState != TaskState::None && block.editListItem && ContainsPoint(block.markerRect, point)) { return { .kind = MarkdownArticleEditControlHitKind::TaskMarker, .listItem = *block.editListItem, }; } if (!block.children.empty()) { return EditControlHitForBlocks(block.children, point); } return {}; } [[nodiscard]] MarkdownArticleEditControlHit EditControlHitForDetailsBlock( const LaidOutBlock &block, QPoint point) { if (ContainsPoint(block.headerRect, point) && block.editBlock) { const auto leftWidth = std::max( block.textRect.left() - block.headerRect.left(), 0); const auto leftToggleRect = QRect( block.headerRect.left(), block.headerRect.top(), leftWidth, block.headerRect.height()); if (ContainsPoint(leftToggleRect, point) || (!block.actionRect.isEmpty() && ContainsPoint(block.actionRect, point))) { return { .kind = MarkdownArticleEditControlHitKind::DetailsToggle, .block = *block.editBlock, }; } return {}; } if (!block.children.empty()) { return EditControlHitForBlocks(block.children, point); } return {}; } [[nodiscard]] MarkdownArticleEditControlHit EditControlHitForBlock( const LaidOutBlock &block, QPoint point) { switch (block.kind) { case PreparedBlockKind::ListItem: return EditControlHitForListItemBlock(block, point); case PreparedBlockKind::Details: return EditControlHitForDetailsBlock(block, point); default: if (!block.children.empty()) { return EditControlHitForBlocks(block.children, point); } return {}; } } [[nodiscard]] MarkdownArticleEditControlHit EditControlHitForBlocks( const std::vector &blocks, QPoint point) { for (const auto &block : blocks) { if (ContainsPoint(block.outer, point)) { return EditControlHitForBlock(block, point); } } return {}; } [[nodiscard]] PreparedEditHit EditHitForBlock( const LaidOutBlock &block, QPoint point) { switch (block.kind) { case PreparedBlockKind::List: return EditHitForListBlock(block, point); case PreparedBlockKind::ListItem: return EditHitForListItemBlock(block, point); case PreparedBlockKind::Quote: return EditHitForQuoteBlock(block, point); case PreparedBlockKind::Table: return EditHitForTableBlock(block, point); case PreparedBlockKind::Details: return EditHitForDetailsBlock(block, point); case PreparedBlockKind::DisplayMath: if (block.editLeaf) { return EditHitFromLeafSource(*block.editLeaf, true); } break; case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::CodeBlock: case PreparedBlockKind::Rule: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::EmbedPost: case PreparedBlockKind::Placeholder: if (ContainsPoint(VisibleTextRectForBlock(block), point) && block.editLeaf) { return EditHitFromLeafSource(*block.editLeaf, true); } break; } return EditFallbackHitForBlock(block); } [[nodiscard]] std::optional EditableLeafForSegment( const SelectableSegment &segment) { if (segment.cell) { return segment.cell->editLeaf; } else if (IsDisplayMathSegment(segment) && segment.block) { return segment.block->editLeaf; } else if (segment.block && (segment.leaf == &segment.block->leaf)) { return segment.block->editLeaf; } return std::nullopt; } [[nodiscard]] PreparedEditBlockContainerPath BlockChildContainer( const PreparedEditBlockSource &source) { auto result = source.path.container; result.steps.push_back({ .kind = PreparedEditBlockContainerKind::BlockChildren, .blockIndex = source.path.index, }); return result; } [[nodiscard]] std::optional ContainerPathForChildBlock( const LaidOutBlock &block) { if (block.editBlock && ValidBlockPath(block.editBlock->path)) { return block.editBlock->path.container; } else if (block.editListItem && ValidBlockPath(block.editListItem->block)) { return block.editListItem->block.container; } else if (block.editLeaf && ValidBlockPath(block.editLeaf->block)) { return block.editLeaf->block.container; } return std::nullopt; } [[nodiscard]] std::optional ContainerPathForBlocks( const std::vector &blocks) { for (const auto &block : blocks) { if (const auto result = ContainerPathForChildBlock(block)) { return result; } } return std::nullopt; } [[nodiscard]] std::optional BlockChildrenContainerPath( const LaidOutBlock &block) { if (block.editBlock && ValidBlockPath(block.editBlock->path)) { return BlockChildContainer(*block.editBlock); } return ContainerPathForBlocks(block.children); } [[nodiscard]] std::optional ListItemChildrenContainerPath( const LaidOutBlock &block) { if (block.editListItem && ValidBlockPath(block.editListItem->block)) { return ListItemChildContainer(*block.editListItem); } return ContainerPathForBlocks(block.children); } [[nodiscard]] std::optional ListBlockPath( const LaidOutBlock &block) { if (block.editBlock && ValidBlockPath(block.editBlock->path)) { return block.editBlock->path; } for (const auto &child : block.children) { if (child.editListItem && ValidBlockPath(child.editListItem->block)) { return child.editListItem->block; } } return std::nullopt; } [[nodiscard]] int DistanceToRect(QPoint point, QRect rect) { if (rect.isEmpty()) { return std::numeric_limits::max(); } auto dx = 0; if (point.x() < rect.left()) { dx = rect.left() - point.x(); } else if (point.x() > rect.right()) { dx = point.x() - rect.right(); } auto dy = 0; if (point.y() < rect.top()) { dy = rect.top() - point.y(); } else if (point.y() > rect.bottom()) { dy = point.y() - rect.bottom(); } return dx + dy; } [[nodiscard]] int GapIndicatorY( QRect before, QRect after, QRect containerRect) { auto result = 0; if (!before.isEmpty() && !after.isEmpty()) { const auto top = before.y() + before.height(); const auto bottom = after.y(); result = (top <= bottom) ? ((top + bottom) / 2) : top; } else if (!before.isEmpty()) { result = before.y() + before.height(); } else if (!after.isEmpty()) { result = after.y(); } else if (!containerRect.isEmpty()) { result = containerRect.y(); } if (!containerRect.isEmpty()) { result = std::clamp( result, containerRect.y(), containerRect.y() + containerRect.height()); } return result; } [[nodiscard]] QRect GapIndicatorRect( QRect before, QRect after, QRect containerRect) { auto span = QRect(); if (!before.isEmpty()) { span = before; } if (!after.isEmpty()) { span = span.isEmpty() ? after : span.united(after); } if (span.isEmpty()) { span = containerRect; } if (span.isEmpty()) { return QRect(); } auto left = span.x(); auto right = span.x() + span.width(); if (!containerRect.isEmpty()) { const auto containerLeft = containerRect.x(); const auto containerRight = containerRect.x() + containerRect.width(); left = std::clamp(left, containerLeft, containerRight); right = std::clamp(right, containerLeft, containerRight); if (right <= left) { left = containerLeft; right = containerRight; } } if (right <= left) { return QRect(); } return QRect( left, GapIndicatorY(before, after, containerRect), right - left, 1); } [[nodiscard]] QRect ListItemGapSpanRect(const LaidOutBlock &block) { return block.contentRect.isEmpty() ? block.outer : block.contentRect; } [[nodiscard]] MarkdownArticleDropLocation EditDropLocationForBlock( const LaidOutBlock &block, QPoint point); struct DropGapCandidate { MarkdownArticleDropLocation location; int distance = std::numeric_limits::max(); }; [[nodiscard]] bool PreparedEditContainerHasPrefix( 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 PreparedEditIndexInRange(int index, int from, int till) { return (index >= from) && (index < till); } [[nodiscard]] bool PreparedEditPathInBlockRange( const PreparedEditBlockPath &path, const PreparedEditBlockRange &range) { if (path.container == range.container) { return PreparedEditIndexInRange(path.index, range.from, range.till); } if (!PreparedEditContainerHasPrefix(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 PreparedEditIndexInRange(step.blockIndex, range.from, range.till); } [[nodiscard]] bool PreparedEditPathInListItemRange( const PreparedEditBlockPath &path, const PreparedEditListItemRange &range) { if (!PreparedEditContainerHasPrefix(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) && PreparedEditIndexInRange(step.listItemIndex, range.from, range.till); } [[nodiscard]] bool PreparedEditContainerNestedInSelection( const PreparedEditBlockContainerPath &container, const PreparedEditSelection &selection) { const auto marker = PreparedEditBlockPath{ .container = container, .index = 0, }; switch (selection.kind) { case PreparedEditSelectionKind::Blocks: return (container.steps.size() > selection.blocks.container.steps.size()) && PreparedEditPathInBlockRange(marker, selection.blocks); case PreparedEditSelectionKind::ListItems: return (container.steps.size() > selection.listItems.block.container.steps.size()) && PreparedEditPathInListItemRange(marker, selection.listItems); case PreparedEditSelectionKind::TableRows: case PreparedEditSelectionKind::TableCells: case PreparedEditSelectionKind::None: return false; } return false; } [[nodiscard]] bool PreparedEditBlockPathInSelection( const PreparedEditBlockPath &path, const PreparedEditSelection &selection) { switch (selection.kind) { case PreparedEditSelectionKind::Blocks: return PreparedEditPathInBlockRange(path, selection.blocks); case PreparedEditSelectionKind::ListItems: return PreparedEditPathInListItemRange(path, selection.listItems); case PreparedEditSelectionKind::TableRows: case PreparedEditSelectionKind::TableCells: case PreparedEditSelectionKind::None: return false; } return false; } [[nodiscard]] bool StructuralDropTargetSupported( const PreparedEditBlockDropTarget &target, const PreparedEditSelection &selection) { if ((selection.kind != PreparedEditSelectionKind::Blocks) || selection.blocks.empty()) { return false; } if ((target.container == selection.blocks.container) && (target.insertIndex >= selection.blocks.from) && (target.insertIndex <= selection.blocks.till)) { return false; } return !PreparedEditContainerNestedInSelection( target.container, selection); } [[nodiscard]] bool StructuralDropTargetSupported( const PreparedEditListItemDropTarget &target, const PreparedEditSelection &selection) { if ((selection.kind != PreparedEditSelectionKind::ListItems) || selection.listItems.empty()) { return false; } if ((target.block == selection.listItems.block) && (target.insertIndex >= selection.listItems.from) && (target.insertIndex <= selection.listItems.till)) { return false; } return !PreparedEditBlockPathInSelection(target.block, selection); } void UniteNonEmptyRect(QRect *result, QRect rect) { if (!result || rect.isEmpty()) { return; } *result = result->isEmpty() ? rect : result->united(rect); } void AddSelectedBlockRects( QRect *result, const std::vector &blocks, const PreparedEditBlockRange &range) { for (const auto &block : blocks) { if (block.editBlock && (block.editBlock->path.container == range.container) && PreparedEditIndexInRange( block.editBlock->path.index, range.from, range.till)) { UniteNonEmptyRect(result, block.outer); } AddSelectedBlockRects(result, block.children, range); } } [[nodiscard]] bool AddSelectedListItemRects( QRect *result, const std::vector &blocks, const PreparedEditListItemRange &range) { for (const auto &block : blocks) { if (block.kind == PreparedBlockKind::List) { if (const auto listBlock = ListBlockPath(block); listBlock && (*listBlock == range.block)) { const auto count = int(block.children.size()); const auto from = std::clamp(range.from, 0, count); const auto till = std::clamp(range.till, from, count); for (auto i = from; i != till; ++i) { UniteNonEmptyRect( result, ListItemGapSpanRect(block.children[i])); } return true; } } if (AddSelectedListItemRects(result, block.children, range)) { return true; } } return false; } [[nodiscard]] QRect StructuralSelectionRect( const std::vector &blocks, const PreparedEditSelection &selection) { auto result = QRect(); switch (selection.kind) { case PreparedEditSelectionKind::Blocks: AddSelectedBlockRects(&result, blocks, selection.blocks); return result; case PreparedEditSelectionKind::ListItems: static_cast(AddSelectedListItemRects( &result, blocks, selection.listItems)); return result; case PreparedEditSelectionKind::TableRows: case PreparedEditSelectionKind::TableCells: case PreparedEditSelectionKind::None: return {}; } return {}; } [[nodiscard]] bool PointInsideSelectionVerticalSpan( QPoint point, const std::vector &blocks, const PreparedEditSelection &selection) { const auto rect = StructuralSelectionRect(blocks, selection); return !rect.isEmpty() && (point.y() >= rect.top()) && (point.y() <= rect.bottom()); } template void ConsiderGapCandidate( DropGapCandidate *best, Target target, QRect before, QRect after, QRect containerRect, QPoint point, const PreparedEditSelection *selection = nullptr) { if (!best) { return; } if (selection && !StructuralDropTargetSupported(target, *selection)) { return; } const auto indicatorRect = GapIndicatorRect(before, after, containerRect); if (indicatorRect.isEmpty()) { return; } const auto distance = DistanceToRect(point, indicatorRect); if (distance >= best->distance) { return; } best->distance = distance; best->location.target = PreparedEditDropTarget(std::move(target)); best->location.indicatorRect = indicatorRect; } void ConsiderBlockContainerGapCandidates( DropGapCandidate *best, const std::vector &blocks, QPoint point, std::optional container, QRect containerRect, const PreparedEditSelection *selection = nullptr) { if (!container) { return; } const auto count = int(blocks.size()); for (auto i = 0; i != count + 1; ++i) { ConsiderGapCandidate( best, PreparedEditBlockDropTarget{ .container = *container, .insertIndex = i, }, (i > 0) ? blocks[i - 1].outer : QRect(), (i < count) ? blocks[i].outer : QRect(), containerRect, point, selection); } } void ConsiderListItemGapCandidates( DropGapCandidate *best, const std::vector &blocks, QPoint point, std::optional listBlock, QRect containerRect, const PreparedEditSelection *selection = nullptr) { if (!(listBlock && ValidBlockPath(*listBlock))) { return; } const auto count = int(blocks.size()); for (auto i = 0; i != count + 1; ++i) { ConsiderGapCandidate( best, PreparedEditListItemDropTarget{ .block = *listBlock, .insertIndex = i, }, (i > 0) ? ListItemGapSpanRect(blocks[i - 1]) : QRect(), (i < count) ? ListItemGapSpanRect(blocks[i]) : QRect(), containerRect, point, selection); } } [[nodiscard]] MarkdownArticleDropLocation EditDropLocationForBlockContainer( const std::vector &blocks, QPoint point, std::optional container, QRect containerRect) { for (const auto &block : blocks) { if (ContainsPoint(block.outer, point)) { if (const auto nested = EditDropLocationForBlock(block, point); nested.valid()) { return nested; } break; } } auto best = DropGapCandidate(); ConsiderBlockContainerGapCandidates( &best, blocks, point, std::move(container), containerRect); return best.location; } [[nodiscard]] MarkdownArticleDropLocation EditDropLocationForListItems( const std::vector &blocks, QPoint point, std::optional listBlock, QRect containerRect) { for (const auto &block : blocks) { if (ContainsPoint(block.outer, point)) { if (const auto nested = EditDropLocationForBlock(block, point); nested.valid()) { return nested; } break; } } auto best = DropGapCandidate(); ConsiderListItemGapCandidates( &best, blocks, point, std::move(listBlock), containerRect); return best.location; } void ConsiderStructuralBlockDropTargets( DropGapCandidate *best, const std::vector &blocks, QPoint point, std::optional container, QRect containerRect, const PreparedEditSelection &selection) { ConsiderBlockContainerGapCandidates( best, blocks, point, std::move(container), containerRect, &selection); for (const auto &block : blocks) { switch (block.kind) { case PreparedBlockKind::List: for (const auto &child : block.children) { if ((child.kind == PreparedBlockKind::ListItem) && ContainsPoint(child.contentRect, point)) { ConsiderStructuralBlockDropTargets( best, child.children, point, ListItemChildrenContainerPath(child), child.contentRect, selection); } } break; case PreparedBlockKind::ListItem: if (ContainsPoint(block.contentRect, point)) { ConsiderStructuralBlockDropTargets( best, block.children, point, ListItemChildrenContainerPath(block), block.contentRect, selection); } break; case PreparedBlockKind::Quote: case PreparedBlockKind::Details: if (ContainsPoint(block.contentRect, point)) { ConsiderStructuralBlockDropTargets( best, block.children, point, BlockChildrenContainerPath(block), block.contentRect, selection); } break; case PreparedBlockKind::Table: case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::CodeBlock: case PreparedBlockKind::Rule: case PreparedBlockKind::DisplayMath: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::EmbedPost: case PreparedBlockKind::Placeholder: break; } } } void ConsiderStructuralListItemDropTargets( DropGapCandidate *best, const std::vector &blocks, QPoint point, const PreparedEditBlockPath &sourceListBlock, const PreparedEditSelection &selection) { for (const auto &block : blocks) { switch (block.kind) { case PreparedBlockKind::List: if (const auto listBlock = ListBlockPath(block); listBlock && (*listBlock == sourceListBlock)) { ConsiderListItemGapCandidates( best, block.children, point, sourceListBlock, block.contentRect.isEmpty() ? block.outer : block.contentRect, &selection); } for (const auto &child : block.children) { if (child.kind == PreparedBlockKind::ListItem) { ConsiderStructuralListItemDropTargets( best, child.children, point, sourceListBlock, selection); } } break; case PreparedBlockKind::ListItem: case PreparedBlockKind::Quote: case PreparedBlockKind::Details: ConsiderStructuralListItemDropTargets( best, block.children, point, sourceListBlock, selection); break; case PreparedBlockKind::Table: case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::CodeBlock: case PreparedBlockKind::Rule: case PreparedBlockKind::DisplayMath: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::EmbedPost: case PreparedBlockKind::Placeholder: break; } } } [[nodiscard]] MarkdownArticleDropLocation EditDropLocationForTableCell( const LaidOutTableCell &cell, QPoint point) { return ContainsPoint(cell.outer, point) ? MarkdownArticleDropLocation() : MarkdownArticleDropLocation(); } [[nodiscard]] MarkdownArticleDropLocation EditDropLocationForTableRow( const LaidOutTableRow &row, QPoint point) { for (const auto &cell : row.cells) { if (ContainsPoint(cell.outer, point)) { return EditDropLocationForTableCell(cell, point); } } return ContainsPoint(row.outer, point) ? MarkdownArticleDropLocation() : MarkdownArticleDropLocation(); } [[nodiscard]] MarkdownArticleDropLocation EditDropLocationForBlock( const LaidOutBlock &block, QPoint point) { switch (block.kind) { case PreparedBlockKind::List: return EditDropLocationForListItems( block.children, point, ListBlockPath(block), block.contentRect.isEmpty() ? block.outer : block.contentRect); case PreparedBlockKind::ListItem: if (ContainsPoint(block.contentRect, point)) { return EditDropLocationForBlockContainer( block.children, point, ListItemChildrenContainerPath(block), block.contentRect); } return {}; case PreparedBlockKind::Quote: case PreparedBlockKind::Details: if (ContainsPoint(block.contentRect, point)) { return EditDropLocationForBlockContainer( block.children, point, BlockChildrenContainerPath(block), block.contentRect); } return {}; case PreparedBlockKind::Table: for (const auto &row : block.tableRows) { if (ContainsPoint(row.outer, point)) { return EditDropLocationForTableRow(row, point); } } return {}; case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::CodeBlock: case PreparedBlockKind::Rule: case PreparedBlockKind::DisplayMath: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::EmbedPost: case PreparedBlockKind::Placeholder: return {}; } return {}; } [[nodiscard]] bool ToggleDetailsBlock( std::vector *blocks, const QString &anchorId) { if (!blocks) { return false; } for (auto &block : *blocks) { if (block.kind == PreparedBlockKind::Details && block.anchorId == anchorId) { block.collapsed = !block.collapsed; return true; } if (ToggleDetailsBlock(&block.children, anchorId)) { return true; } } return false; } [[nodiscard]] bool PreparedBlockHasAnchor( const PreparedBlock &block, const QString &anchorId) { return block.anchorId == anchorId || ranges::contains(block.anchorIds, anchorId); } [[nodiscard]] MarkdownArticleAnchorExpansion ExpandDetailsToAnchor( std::vector *blocks, const QString &anchorId, bool expandTarget) { if (!blocks || anchorId.isEmpty()) { return {}; } for (auto &block : *blocks) { auto result = MarkdownArticleAnchorExpansion(); if (PreparedBlockHasAnchor(block, anchorId)) { result.found = true; if (!expandTarget) { return result; } } else { result = ExpandDetailsToAnchor( &block.children, anchorId, expandTarget); } if (result.found) { if (block.kind == PreparedBlockKind::Details && block.collapsed) { block.collapsed = false; result.changed = true; } return result; } } return {}; } [[nodiscard]] const PreparedBlock *FindPreparedDetailsBlock( const std::vector &blocks, const QString &anchorId) { for (const auto &block : blocks) { if (block.kind == PreparedBlockKind::Details && block.anchorId == anchorId) { return █ } if (const auto nested = FindPreparedDetailsBlock( block.children, anchorId)) { return nested; } } return nullptr; } void AppendPreparedBlocksText( const std::vector &blocks, QString *to) { const auto append = [&](const QString &text) { if (text.isEmpty()) { return; } if (!to->isEmpty()) { to->append('\n'); } to->append(text); }; for (const auto &block : blocks) { append(block.text.text); for (const auto &row : block.tableRows) { for (const auto &cell : row.cells) { append(cell.text.text); } } AppendPreparedBlocksText(block.children, to); } } void ClearColorizedFormulaImages(std::vector *blocks) { if (!blocks) { return; } for (auto &block : *blocks) { block.colorizedFormulaImage = QImage(); block.colorizedFormulaColor = QColor(); block.colorizedFormulaSize = QSize(); ClearColorizedFormulaImages(&block.children); } } void HideLeafSpoilers(Ui::Text::String *leaf) { if (leaf->hasSpoilers()) { leaf->setSpoilerRevealed(false, anim::type::instant); } } void HideTableCellSpoilers(LaidOutTableCell *cell) { HideLeafSpoilers(&cell->leaf); HideLeafSpoilers(&cell->placeholderLeaf); } void HideBlockSpoilers(LaidOutBlock *block) { HideLeafSpoilers(&block->leaf); HideLeafSpoilers(&block->placeholderLeaf); HideLeafSpoilers(&block->labelLeaf); HideLeafSpoilers(&block->subtitleLeaf); HideLeafSpoilers(&block->actionLeaf); HideLeafSpoilers(&block->marker); HideLeafSpoilers(&block->fallbackLeaf); for (auto &row : block->tableRows) { for (auto &cell : row.cells) { HideTableCellSpoilers(&cell); } } for (auto &child : block->children) { HideBlockSpoilers(&child); } } void HideBlocksSpoilers(std::vector *blocks) { for (auto &block : *blocks) { HideBlockSpoilers(&block); } } struct PreparedArticleLeafLookup { PreparedBlock *block = nullptr; PreparedTableCell *cell = nullptr; [[nodiscard]] bool valid() const { return block || cell; } }; struct ConstPreparedArticleLeafLookup { const PreparedBlock *block = nullptr; const PreparedTableCell *cell = nullptr; [[nodiscard]] bool valid() const { return block || cell; } }; struct LaidOutArticleLeafLookup { LaidOutBlock *block = nullptr; LaidOutTableCell *cell = nullptr; [[nodiscard]] bool valid() const { return block || cell; } }; [[nodiscard]] PreparedBlock *FindPreparedArticleBlockByPath( std::vector *blocks, const PreparedEditBlockPath &path) { for (auto &block : *blocks) { if (block.editBlock && (block.editBlock->path == path)) { return █ } if (const auto nested = FindPreparedArticleBlockByPath( &block.children, path)) { return nested; } } return nullptr; } [[nodiscard]] const PreparedBlock *FindPreparedArticleBlockByPath( const std::vector &blocks, const PreparedEditBlockPath &path) { for (const auto &block : blocks) { if (block.editBlock && (block.editBlock->path == path)) { return █ } if (const auto nested = FindPreparedArticleBlockByPath( block.children, path)) { return nested; } } return nullptr; } [[nodiscard]] PreparedBlock *FindPreparedArticleLeafBlock( PreparedBlock *block, const PreparedEditLeafSource &source) { if (!block) { return nullptr; } if (block->editLeaf && (*block->editLeaf == source)) { return block; } for (auto &child : block->children) { if (const auto nested = FindPreparedArticleLeafBlock(&child, source)) { return nested; } } return nullptr; } [[nodiscard]] const PreparedBlock *FindPreparedArticleLeafBlock( const PreparedBlock *block, const PreparedEditLeafSource &source) { if (!block) { return nullptr; } if (block->editLeaf && (*block->editLeaf == source)) { return block; } for (const auto &child : block->children) { if (const auto nested = FindPreparedArticleLeafBlock(&child, source)) { return nested; } } return nullptr; } [[nodiscard]] PreparedTableCell *FindPreparedArticleLeafCell( PreparedBlock *block, const PreparedEditLeafSource &source) { if (!block) { return nullptr; } for (auto &row : block->tableRows) { for (auto &cell : row.cells) { if (cell.editLeaf && (*cell.editLeaf == source)) { return &cell; } } } return nullptr; } [[nodiscard]] const PreparedTableCell *FindPreparedArticleLeafCell( const PreparedBlock *block, const PreparedEditLeafSource &source) { if (!block) { return nullptr; } for (const auto &row : block->tableRows) { for (const auto &cell : row.cells) { if (cell.editLeaf && (*cell.editLeaf == source)) { return &cell; } } } return nullptr; } [[nodiscard]] PreparedArticleLeafLookup FindPreparedArticleLeaf( std::vector *blocks, const PreparedEditLeafSource &source) { const auto owner = FindPreparedArticleBlockByPath(blocks, source.block); if (!owner) { return {}; } switch (source.kind) { case PreparedEditLeafKind::TableCellText: return { .cell = FindPreparedArticleLeafCell(owner, source) }; case PreparedEditLeafKind::BlockText: case PreparedEditLeafKind::BlockCaption: case PreparedEditLeafKind::ListItemText: case PreparedEditLeafKind::MathFormula: return { .block = FindPreparedArticleLeafBlock(owner, source) }; } return {}; } [[nodiscard]] ConstPreparedArticleLeafLookup FindPreparedArticleLeaf( const std::vector &blocks, const PreparedEditLeafSource &source) { const auto owner = FindPreparedArticleBlockByPath(blocks, source.block); if (!owner) { return {}; } switch (source.kind) { case PreparedEditLeafKind::TableCellText: return { .cell = FindPreparedArticleLeafCell(owner, source) }; case PreparedEditLeafKind::BlockText: case PreparedEditLeafKind::BlockCaption: case PreparedEditLeafKind::ListItemText: case PreparedEditLeafKind::MathFormula: return { .block = FindPreparedArticleLeafBlock(owner, source) }; } return {}; } [[nodiscard]] LaidOutBlock *FindLaidOutArticleBlockByPath( std::vector *blocks, const PreparedEditBlockPath &path) { for (auto &block : *blocks) { if (block.editBlock && (block.editBlock->path == path)) { return █ } if (const auto nested = FindLaidOutArticleBlockByPath( &block.children, path)) { return nested; } } return nullptr; } [[nodiscard]] LaidOutBlock *FindLaidOutArticleLeafBlock( LaidOutBlock *block, const PreparedEditLeafSource &source) { if (!block) { return nullptr; } if (block->editLeaf && (*block->editLeaf == source)) { return block; } for (auto &child : block->children) { if (const auto nested = FindLaidOutArticleLeafBlock(&child, source)) { return nested; } } return nullptr; } [[nodiscard]] LaidOutTableCell *FindLaidOutArticleLeafCell( LaidOutBlock *block, const PreparedEditLeafSource &source) { if (!block) { return nullptr; } for (auto &row : block->tableRows) { for (auto &cell : row.cells) { if (cell.editLeaf && (*cell.editLeaf == source)) { return &cell; } } } return nullptr; } [[nodiscard]] LaidOutArticleLeafLookup FindLaidOutArticleLeaf( std::vector *blocks, const PreparedEditLeafSource &source) { const auto owner = FindLaidOutArticleBlockByPath(blocks, source.block); if (!owner) { return {}; } switch (source.kind) { case PreparedEditLeafKind::TableCellText: return { .cell = FindLaidOutArticleLeafCell(owner, source) }; case PreparedEditLeafKind::BlockText: case PreparedEditLeafKind::BlockCaption: case PreparedEditLeafKind::ListItemText: case PreparedEditLeafKind::MathFormula: return { .block = FindLaidOutArticleLeafBlock(owner, source) }; } return {}; } void PatchPreparedArticleLeaf( PreparedBlock *block, const PreparedBlock &prepared, const PreparedEditLeafSource &source) { block->text = prepared.text; block->links = prepared.links; block->editPlaceholderText = prepared.editPlaceholderText; if (source.kind == PreparedEditLeafKind::MathFormula) { block->formulaTex = prepared.formulaTex; block->formulaIndex = prepared.formulaIndex; } } void PatchPreparedArticleLeaf( PreparedTableCell *cell, const PreparedTableCell &prepared) { cell->text = prepared.text; cell->links = prepared.links; cell->editPlaceholderText = prepared.editPlaceholderText; } void CollectCodeBlockHighlightKeys( const std::vector &blocks, std::unordered_set< PendingHighlightKey, PendingHighlightKeyHasher> *keys) { for (const auto &block : blocks) { if (block.kind == PreparedBlockKind::CodeBlock && !block.codeLanguage.isEmpty()) { keys->insert({ .text = CodeBlockDisplayText(block.text.text), .language = block.codeLanguage, }); } CollectCodeBlockHighlightKeys(block.children, keys); } } [[nodiscard]] bool ExpectsMediaBlock(const PreparedBlock &prepared) { switch (prepared.kind) { case PreparedBlockKind::Photo: return prepared.photo.id && prepared.photo.viewerOpen && prepared.photo.urlOverride.isEmpty(); case PreparedBlockKind::Video: return bool(prepared.video.id); case PreparedBlockKind::Map: return bool(prepared.map.id); case PreparedBlockKind::Audio: return bool(prepared.audio.id); case PreparedBlockKind::GroupedMedia: return bool(prepared.groupedMedia.id); default: return false; } } } // namespace PlaceholderBlockRuntime::PlaceholderBlockRuntime(Fn repaint) : clickHandler(std::make_shared([] { })) , loadingAnimation( [repaint = std::move(repaint)] { if (repaint) { repaint(); } }, st::defaultInfiniteRadialAnimation) { } TaskMarkerRippleRuntime::TaskMarkerRippleRuntime(Fn repaint) : repaint(std::move(repaint)) { } class MarkdownArticle::Impl final : public CodeBlockSyntaxHighlightTracker { public: Impl( const style::Markdown &st, std::shared_ptr renderer); void setRenderer(std::shared_ptr renderer); void setMediaBlockHost(MediaBlockHost *host); void setMediaPixelScale(double scale); void setTextRepaintCallbacks( Fn repaint, Fn repaintRect, Fn spoilerLinkFilter); void setContent(MarkdownArticleContent content); void setSearchMatches( std::vector matches, int current); [[nodiscard]] auto searchSources() const -> std::vector; void updatePreparedLeaf( const PreparedEditLeafSource &source, const MarkdownArticleContent &prepared); void setEditableMaxLineWidthOverride( const PreparedEditLeafSource &source, int width); void setEditableTextEmptyOverride( const PreparedEditLeafSource &source, bool empty); void setEditableHeightOverride(int editableIndex, int height); void setEditableHeightOverrideForSegment(int segmentIndex, int height); void clearEditableMaxLineWidthOverride(); void clearEditableTextEmptyOverride(); void clearEditableHeightOverride(); void setTextLeafHeightOverride(int textLeafIndex, int height); void clearTextLeafHeightOverride(); [[nodiscard]] int maxWidth(); [[nodiscard]] int lastLayoutWidth() const; [[nodiscard]] bool hasMissingMediaBlocks() const; [[nodiscard]] int resizeGetHeight(int width); [[nodiscard]] auto countRevealLinesGeometry(int width) -> std::vector; void setVisibleTopBottom(int visibleTop, int visibleBottom); void paint(Painter &p, const MarkdownArticlePaintContext &context); [[nodiscard]] MarkdownArticleHitTestResult hitTest( QPoint point, Ui::Text::StateRequest::Flags flags) const; [[nodiscard]] PreparedEditHit editHitTest(QPoint point) const; [[nodiscard]] std::vector mediaBlockGeometries() const; void setGroupedActiveIndex( const PreparedEditBlockSource &source, int index); [[nodiscard]] MarkdownArticleDropLocation editDropTarget( QPoint point) const; [[nodiscard]] MarkdownArticleDropLocation editBlockDropTarget( QPoint point) const; [[nodiscard]] MarkdownArticleDropLocation editStructuralDropTarget( QPoint point, const PreparedEditSelection &selection) const; [[nodiscard]] MarkdownArticleEditControlHit editControlHitTest( QPoint point) const; [[nodiscard]] MarkdownArticleHorizontalScrollHit horizontalScrollHit( QPoint point) const; [[nodiscard]] bool canConsumeHorizontalScroll( QPoint point, int delta) const; [[nodiscard]] bool consumeHorizontalScroll(QPoint point, int delta); [[nodiscard]] bool beginHorizontalScroll(QPoint point, bool fromTouch); [[nodiscard]] bool updateHorizontalScroll(QPoint point); void endHorizontalScroll(); [[nodiscard]] int anchorTop(const QString &anchorId) const; [[nodiscard]] auto scrollAnchorForTop(int top) const -> std::optional; [[nodiscard]] int scrollTopForAnchor( const MarkdownArticleScrollAnchor &anchor) const; [[nodiscard]] MarkdownArticleAnchorExpansion expandDetailsToAnchor( const QString &anchorId); [[nodiscard]] MarkdownArticleAnchorExpansion expandDetailsBlock( const QString &anchorId); [[nodiscard]] bool toggleDetails(const QString &anchorId); [[nodiscard]] bool segmentIsText(int index) const; [[nodiscard]] bool segmentIsDisplayMath(int index) const; [[nodiscard]] bool segmentIsEditable(int index) const; [[nodiscard]] int segmentLength(int index) const; [[nodiscard]] int firstTextSegmentIndex() const; [[nodiscard]] int firstEditableSegmentIndex() const; [[nodiscard]] int textLeafIndexForSegment(int segmentIndex) const; [[nodiscard]] int segmentIndexForTextLeafIndex(int textLeafIndex) const; [[nodiscard]] int editableIndexForSegment(int segmentIndex) const; [[nodiscard]] int segmentIndexForEditableIndex(int editableIndex) const; [[nodiscard]] auto editableLeafForSegment(int segmentIndex) const -> std::optional; [[nodiscard]] int segmentIndexForEditableLeaf( const PreparedEditLeafSource &source) const; [[nodiscard]] QRect textSegmentRect(int segmentIndex) const; [[nodiscard]] QRect logicalSegmentRect(int segmentIndex) const; [[nodiscard]] QRect segmentRect(int segmentIndex) const; [[nodiscard]] QRect displayMathEditRect(int segmentIndex) const; [[nodiscard]] QRect displayMathBlockRect(int segmentIndex) const; [[nodiscard]] int pullquoteAvailableTextWidthForEditableLeaf( const PreparedEditLeafSource &source) const; [[nodiscard]] bool revealSegment(int segmentIndex); [[nodiscard]] MarkdownArticleTextLeafStyle textLeafStyleForSegment( int segmentIndex) const; [[nodiscard]] MarkdownArticleTextLeafStyle editableStyleForSegment( int segmentIndex) const; [[nodiscard]] int selectionOffsetFromHit( const MarkdownArticleHitTestResult &result, TextSelectType selectionType) const; [[nodiscard]] TextSelection adjustSelection( int segmentIndex, TextSelection selection, TextSelectType selectionType) const; [[nodiscard]] bool selectionContains( MarkdownArticleSelection selection, const MarkdownArticleSelectionEndpoints *endpoints, const MarkdownArticleHitTestResult &result) const; [[nodiscard]] TextForMimeData textForContext( const MarkdownArticleHitTestResult &result) const; [[nodiscard]] TextForMimeData textForSelection( MarkdownArticleSelection selection, const MarkdownArticleSelectionEndpoints *endpoints, const PreparedEditSelection *structuralSelection) const; [[nodiscard]] std::vector richPageSliceForSelection( MarkdownArticleSelection selection) const; [[nodiscard]] bool highlightProcessDone( Spellchecker::HighlightProcessId processId); void invalidatePaletteCache(); void invalidateRasterCache(); [[nodiscard]] bool hasHeavyPart() const; void unloadHeavyPart(); void hideSpoilers(); [[nodiscard]] MediaBlockHost *mediaBlockHost() const; void setPlaceholderLoading(PreparedPlaceholderBlockId id); void clearPlaceholderLoading(PreparedPlaceholderBlockId id); void clearAllPlaceholderLoading(); void addTaskMarkerRipple( const PreparedEditListItemSource &source, QPoint point); void addPlaceholderRipple(PreparedPlaceholderBlockId id, QPoint point); void stopPlaceholderRipple(PreparedPlaceholderBlockId id); void invalidateLayout(); private: struct ActiveHorizontalScrollDrag { MarkdownArticleScrollOwnerIdentity owner; QPoint pressPoint; int startScrollLeft = 0; int thumbGrabOffset = 0; bool fromTouch = false; }; [[nodiscard]] int currentDevicePixelRatio() const; void rebuildVisibleSegmentLookup(); void refreshVisibleSegmentSpan(); void clearMediaBlocks(); void refreshMediaBlockHosts(); void clearPlaceholderRuntimes(); [[nodiscard]] std::shared_ptr getOrCreatePlaceholderRuntime(PreparedPlaceholderBlockId id); void prunePlaceholderRuntimes(); void requestPlaceholderRepaint(PreparedPlaceholderBlockId id); [[nodiscard]] auto getOrCreateTaskMarkerRippleRuntime( const PreparedEditListItemSource &source) -> std::shared_ptr; void requestTaskMarkerRepaint( const PreparedEditListItemSource &source); void pruneTaskMarkerRuntimes(); [[nodiscard]] std::shared_ptr getOrCreateMediaBlock( const PreparedBlock &prepared); template [[nodiscard]] std::shared_ptr getOrCreateMediaBlock( PreparedMediaBlockId id, Factory &&factory); [[nodiscard]] Spellchecker::HighlightProcessId tryHighlightSyntax( const QString &displayText, const QString &language, TextWithEntities &marked) override; [[nodiscard]] SegmentSpan candidateSegmentSpan(QPoint point) const; void clearPendingHighlightBlockPointers(); void prunePendingHighlightProcessesForContent(); void registerPendingHighlightProcess( const PendingHighlightKey &key, Spellchecker::HighlightProcessId processId); void registerPendingHighlightBlock(LaidOutBlock &block); void registerPendingHighlightBlocks(std::vector &blocks); void resetFormulaRasterCache(); void setPlaceholderLoadingValue( PreparedPlaceholderBlockId id, bool loading); void invalidateLayout(bool harvestCurrentBlocks); void invalidateGeometry(); [[nodiscard]] const style::Markdown &layoutStyle() const; [[nodiscard]] MarkdownArticleScrollOwnerIdentity scrollOwnerIdentity( const LaidOutBlock &block, const std::vector &preparedPath) const; [[nodiscard]] MarkdownArticleHorizontalScrollLookup findHorizontalScrollOwner(QPoint point) const; [[nodiscard]] MarkdownArticleHorizontalScrollLookup findHorizontalScrollOwner( const std::vector &blocks, const std::vector *prepared, QPoint point, std::vector *preparedPath) const; [[nodiscard]] std::optional findHorizontalScrollOwner(int segmentIndex) const; [[nodiscard]] std::optional findHorizontalScrollOwner( const std::vector &blocks, const std::vector *prepared, const SelectableSegment &segment, std::optional owner, std::vector *preparedPath) const; [[nodiscard]] LaidOutBlock *findScrollOwnerByIdentity( const MarkdownArticleScrollOwnerIdentity &identity); [[nodiscard]] LaidOutBlock *findScrollOwnerByIdentity( std::vector &blocks, const std::vector *prepared, const MarkdownArticleScrollOwnerIdentity &identity, std::vector *preparedPath); void captureScrollState(); void captureScrollState( const std::vector &blocks, const std::vector *prepared, std::vector *preparedPath); void restoreScrollState(); void restoreScrollState( std::vector &blocks, const std::vector *prepared, std::vector *preparedPath); void refreshScrolledGeometry(LaidOutBlock &block); void refreshScrolledGeometry( LaidOutBlock &block, const ActiveHorizontalScrollOwnerState *activeOwner); void refreshScrolledGeometry(std::vector &blocks); void refreshScrolledGeometry( std::vector &blocks, const ActiveHorizontalScrollOwnerState *activeOwner); void updateScrollbarThumb(LaidOutBlock &block); [[nodiscard]] bool setScrollLeft( LaidOutBlock &block, const MarkdownArticleScrollOwnerIdentity &identity, int left); void finalizeRelayout(int heightBottom); void relayout(int width); void relayoutRetained(int width); void retainBlocks(); [[nodiscard]] const LaidOutBlock *pullquoteBlockForEditableLeaf( const PreparedEditLeafSource &source) const; mutable MarkdownArticleContent _content; style::Markdown _style; std::vector _formulaRenders; std::shared_ptr _renderer; std::shared_ptr _inlineFormulaObjects; MediaBlockHost *_mediaBlockHost = nullptr; Fn _textRepaint; Fn _textRepaintRect; Fn _textSpoilerLinkFilter; int _width = -1; int _laidOutWidth = 0; int _height = 0; int _layoutGeneration = 0; double _mediaPixelScale = 1.; MarkdownArticleRevealLineCountsCache _revealLineCounts; CachedTextLeafPool _cachedTextLeafs; std::vector _blocks; std::vector _retainedBlocks; MediaBlockStorage _mediaBlocks; int _missingMediaBlocks = 0; std::unordered_map> _placeholderRuntimes; TaskMarkerRippleRuntimeMap _taskMarkerRippleRuntimes; std::unordered_map< uint64, RelatedArticleImageState> _relatedArticleImages; std::unordered_map< PendingHighlightKey, Spellchecker::HighlightProcessId, PendingHighlightKeyHasher> _pendingHighlightProcesses; std::unordered_map< Spellchecker::HighlightProcessId, PendingHighlightEntry> _pendingHighlightEntries; std::vector> _anchors; std::vector _segments; std::vector _searchMatches; int _currentSearchMatch = -1; std::optional _visibleRange; SegmentSpan _visibleSegmentSpan; std::vector _segmentTops; std::vector _segmentBottoms; std::unordered_map< MarkdownArticleScrollOwnerIdentity, int, MarkdownArticleScrollOwnerIdentityHasher> _capturedScrollLefts; std::optional _activeHorizontalScrollDrag; std::optional _editableMaxLineWidthOverrideLeaf; int _editableMaxLineWidthOverride = 0; std::optional _editableTextEmptyOverrideLeaf; bool _editableTextEmptyOverride = true; int _editableHeightOverrideIndex = -1; int _editableHeightOverride = 0; bool _blocksPainted = false; }; MarkdownArticle::Impl::Impl( const style::Markdown &st, std::shared_ptr renderer) : _style(st) , _renderer(std::move(renderer)) , _inlineFormulaObjects(CreateInlineFormulaObjectCache(_renderer)) { _style.code.font = _style.code.font->monospace(); } void MarkdownArticle::Impl::setRenderer(std::shared_ptr renderer) { _renderer = std::move(renderer); SetInlineFormulaObjectCacheRenderer(_inlineFormulaObjects, _renderer); invalidateRasterCache(); invalidateLayout(); } void MarkdownArticle::Impl::setMediaBlockHost(MediaBlockHost *host) { if (_mediaBlockHost == host) { return; } _mediaBlockHost = host; refreshMediaBlockHosts(); } void MarkdownArticle::Impl::setMediaPixelScale(double scale) { if (_mediaPixelScale == scale) { return; } _mediaPixelScale = scale; invalidateLayout(); } void MarkdownArticle::Impl::setTextRepaintCallbacks( Fn repaint, Fn repaintRect, Fn spoilerLinkFilter) { _textRepaint = std::move(repaint); _textRepaintRect = std::move(repaintRect); _textSpoilerLinkFilter = std::move(spoilerLinkFilter); } void MarkdownArticle::Impl::setContent(MarkdownArticleContent content) { if (hasHeavyPart()) { unloadHeavyPart(); } auto reusedMediaBlocks = MediaBlockStorage(); const auto reuseMediaBlocks = (_content.mediaRuntime == content.mediaRuntime); RebuildCachedTextLeafs( _content.blocks.blocks, &_blocks, layoutStyle(), &_cachedTextLeafs); if (!reuseMediaBlocks) { PruneMediaRuntimeBoundCachedTextLeafs(&_cachedTextLeafs); } if (reuseMediaBlocks) { auto oldMediaBlocks = MediaBlockStorage(); oldMediaBlocks.swap(_mediaBlocks); reusedMediaBlocks = ReuseMediaBlocks( _content.blocks.blocks, &oldMediaBlocks, content.blocks.blocks); } else { clearMediaBlocks(); } clearPlaceholderRuntimes(); _relatedArticleImages.clear(); _content = std::move(content); if (reuseMediaBlocks) { _mediaBlocks = std::move(reusedMediaBlocks); } prunePendingHighlightProcessesForContent(); ClearInlineFormulaObjectCache(_inlineFormulaObjects); resetFormulaRasterCache(); _searchMatches.clear(); _currentSearchMatch = -1; invalidateLayout(false); } void MarkdownArticle::Impl::setSearchMatches( std::vector matches, int current) { _searchMatches = std::move(matches); _currentSearchMatch = (current >= 0 && current < int(_searchMatches.size())) ? current : -1; } auto MarkdownArticle::Impl::searchSources() const -> std::vector { auto result = std::vector(); result.reserve(_segments.size()); for (const auto &segment : _segments) { auto source = MarkdownArticleSearchSource(); if (segment.isTextLeaf()) { source.text = segment.leaf->toString(); } const auto block = segment.block; if (block && block->kind == PreparedBlockKind::Details) { source.detailsAnchorId = block->anchorId; if (block->collapsed) { const auto prepared = FindPreparedDetailsBlock( _content.blocks.blocks, block->anchorId); if (prepared) { AppendPreparedBlocksText( prepared->children, &source.hiddenText); } } } result.push_back(std::move(source)); } return result; } void MarkdownArticle::Impl::updatePreparedLeaf( const PreparedEditLeafSource &source, const MarkdownArticleContent &prepared) { if (!ValidBlockPath(source.block) || (_content.mediaRuntime != prepared.mediaRuntime) || (_content.editMode != prepared.editMode)) { setContent(prepared); return; } const auto current = FindPreparedArticleLeaf(&_content.blocks.blocks, source); const auto incoming = FindPreparedArticleLeaf(prepared.blocks.blocks, source); const auto live = FindLaidOutArticleLeaf(&_blocks, source); if (!current.valid() || !incoming.valid() || !live.valid()) { setContent(prepared); return; } auto codeTextChanged = false; auto displayMathOldIndex = -1; auto displayMathNewIndex = -1; switch (source.kind) { case PreparedEditLeafKind::TableCellText: if (!current.cell || !incoming.cell || !live.cell || (current.cell->header != incoming.cell->header) || (current.cell->alignment != incoming.cell->alignment) || (current.cell->verticalAlignment != incoming.cell->verticalAlignment) || (current.cell->column != incoming.cell->column) || (current.cell->colspan != incoming.cell->colspan) || (current.cell->rowspan != incoming.cell->rowspan)) { setContent(prepared); return; } PatchPreparedArticleLeaf(current.cell, *incoming.cell); break; case PreparedEditLeafKind::BlockText: case PreparedEditLeafKind::BlockCaption: case PreparedEditLeafKind::ListItemText: case PreparedEditLeafKind::MathFormula: if (!current.block || !incoming.block || !live.block || (current.block->kind != incoming.block->kind) || (current.block->codeLanguage != incoming.block->codeLanguage) || ((source.kind == PreparedEditLeafKind::MathFormula) && (current.block->kind != PreparedBlockKind::DisplayMath))) { setContent(prepared); return; } codeTextChanged = (current.block->kind == PreparedBlockKind::CodeBlock) && (CodeBlockDisplayText(current.block->text.text) != CodeBlockDisplayText(incoming.block->text.text)); displayMathOldIndex = current.block->formulaIndex; displayMathNewIndex = incoming.block->formulaIndex; PatchPreparedArticleLeaf(current.block, *incoming.block, source); break; } _content.formulas = prepared.formulas; _formulaRenders.resize(_content.formulas.size()); if (source.kind == PreparedEditLeafKind::MathFormula) { if (displayMathOldIndex >= 0 && displayMathOldIndex < int(_formulaRenders.size())) { _formulaRenders[displayMathOldIndex] = RenderedFormula(); } if (displayMathNewIndex >= 0 && displayMathNewIndex < int(_formulaRenders.size())) { _formulaRenders[displayMathNewIndex] = RenderedFormula(); } if (live.block) { live.block->colorizedFormulaImage = QImage(); live.block->colorizedFormulaColor = QColor(); live.block->colorizedFormulaSize = QSize(); } } auto context = LayoutContext(); context.syntaxHighlightTracker = this; context.repaint = _textRepaint; context.repaintRect = _textRepaintRect; context.spoilerLinkFilter = _textSpoilerLinkFilter; if (live.block && incoming.block) { UpdateLaidOutLeafContent( live.block, *incoming.block, &_content.formulas, _inlineFormulaObjects.get(), _content.mediaRuntime, layoutStyle(), context); } else if (live.cell && incoming.cell) { UpdateLaidOutLeafContent( live.cell, *incoming.cell, &_content.formulas, _inlineFormulaObjects.get(), _content.mediaRuntime, layoutStyle(), source.tableRowIndex, source.tableCellIndex, context); } else { setContent(prepared); return; } if (codeTextChanged) { prunePendingHighlightProcessesForContent(); } if (_width > 0) { invalidateGeometry(); } } void MarkdownArticle::Impl::setEditableMaxLineWidthOverride( const PreparedEditLeafSource &source, int width) { width = std::max(width, 0); if (_editableMaxLineWidthOverrideLeaf && (*_editableMaxLineWidthOverrideLeaf == source) && (_editableMaxLineWidthOverride == width)) { return; } _editableMaxLineWidthOverrideLeaf = source; _editableMaxLineWidthOverride = width; invalidateGeometry(); } void MarkdownArticle::Impl::setEditableTextEmptyOverride( const PreparedEditLeafSource &source, bool empty) { if (_editableTextEmptyOverrideLeaf && (*_editableTextEmptyOverrideLeaf == source) && (_editableTextEmptyOverride == empty)) { return; } _editableTextEmptyOverrideLeaf = source; _editableTextEmptyOverride = empty; invalidateGeometry(); } void MarkdownArticle::Impl::setEditableHeightOverride( int editableIndex, int height) { editableIndex = std::max(editableIndex, -1); height = std::max(height, 0); if (_editableHeightOverrideIndex == editableIndex && _editableHeightOverride == height) { return; } _editableHeightOverrideIndex = editableIndex; _editableHeightOverride = height; invalidateGeometry(); } void MarkdownArticle::Impl::setEditableHeightOverrideForSegment( int segmentIndex, int height) { setEditableHeightOverride(editableIndexForSegment(segmentIndex), height); } void MarkdownArticle::Impl::clearEditableMaxLineWidthOverride() { if (!_editableMaxLineWidthOverrideLeaf && (_editableMaxLineWidthOverride == 0)) { return; } _editableMaxLineWidthOverrideLeaf = std::nullopt; _editableMaxLineWidthOverride = 0; invalidateGeometry(); } void MarkdownArticle::Impl::clearEditableTextEmptyOverride() { if (!_editableTextEmptyOverrideLeaf) { return; } _editableTextEmptyOverrideLeaf = std::nullopt; _editableTextEmptyOverride = true; invalidateGeometry(); } void MarkdownArticle::Impl::clearEditableHeightOverride() { setEditableHeightOverride(-1, 0); } void MarkdownArticle::Impl::setTextLeafHeightOverride( int textLeafIndex, int height) { setEditableHeightOverrideForSegment( segmentIndexForTextLeafIndex(textLeafIndex), height); } void MarkdownArticle::Impl::clearTextLeafHeightOverride() { clearEditableHeightOverride(); } int MarkdownArticle::Impl::maxWidth() { const auto &st = layoutStyle(); return std::max( st.pageMaxWidth, st.pagePadding.left() + st.pagePadding.right() + 1); } int MarkdownArticle::Impl::lastLayoutWidth() const { return _laidOutWidth; } bool MarkdownArticle::Impl::hasMissingMediaBlocks() const { return _missingMediaBlocks > 0; } int MarkdownArticle::Impl::resizeGetHeight(int width) { width = std::max(width, 1); if (_width != width) { if (_blocks.empty()) { relayout(width); } else { relayoutRetained(width); } } return std::max(_height, 1); } auto MarkdownArticle::Impl::countRevealLinesGeometry(int width) -> std::vector { width = std::max(width, 1); if (_width != width) { if (_blocks.empty()) { relayout(width); } else { relayoutRetained(width); } } return CollectRevealLines(_blocks, layoutStyle()); } void MarkdownArticle::Impl::setVisibleTopBottom(int visibleTop, int visibleBottom) { if (visibleBottom <= visibleTop) { _visibleRange = std::nullopt; _visibleSegmentSpan = {}; return; } _visibleRange = LogicalVisibleRange{ .top = visibleTop, .bottom = visibleBottom, }; refreshVisibleSegmentSpan(); } void MarkdownArticle::Impl::paint( Painter &p, const MarkdownArticlePaintContext &context) { const auto &st = layoutStyle(); auto local = context; local.selectionState.segments = &_segments; local.searchState.matches = &_searchMatches; local.searchState.current = _currentSearchMatch; const auto &paintSt = local.paintMarkdownStyle(st); auto textPalette = paintSt.textPalette; auto markBg = MarkBgColorForStyle(paintSt); const auto ownedMarkBg = style::internal::OwnedColor(markBg); textPalette.markBg = ownedMarkBg.color(); if (local.reveal) { if (_revealLineCounts.layoutGeneration != _layoutGeneration) { _revealLineCounts.layoutGeneration = _layoutGeneration; _revealLineCounts.counts.clear(); } local.reveal->lineCounts = &_revealLineCounts; } else if (!_revealLineCounts.counts.empty()) { _revealLineCounts = {}; } const auto &previousTextPalette = p.textPalette(); p.setTextPalette(textPalette); PaintBlocks( p, _blocks, &_content.formulas, &_formulaRenders, _renderer.get(), currentDevicePixelRatio(), std::max(_width, 1), st, local); p.setTextPalette(previousTextPalette); _retainedBlocks.clear(); _blocksPainted = true; } MarkdownArticleHitTestResult MarkdownArticle::Impl::hitTest( QPoint point, Ui::Text::StateRequest::Flags flags) const { const auto span = candidateSegmentSpan(point); for (auto i = span.from; i != span.till; ++i) { const auto &segment = _segments[i]; if (const auto result = HitCodeBlockHeader(segment, point, flags); result.valid()) { return result; } } for (auto i = span.from; i != span.till; ++i) { const auto &segment = _segments[i]; if (const auto result = HitTextSegment(segment, point, flags); result.valid()) { return result; } } for (auto i = span.from; i != span.till; ++i) { const auto &segment = _segments[i]; if (const auto result = HitBlockSegment(segment, point, flags); result.valid()) { return result; } } if (flags & Ui::Text::StateRequest::Flag::LookupSymbol) { return HitSegmentFallback(_segments, span, point); } return {}; } PreparedEditHit MarkdownArticle::Impl::editHitTest(QPoint point) const { return EditHitForBlocks(_blocks, point); } std::vector MarkdownArticle::Impl::mediaBlockGeometries() const { auto result = std::vector(); CollectMediaBlockGeometries(&result, _blocks); return result; } void MarkdownArticle::Impl::setGroupedActiveIndex( const PreparedEditBlockSource &source, int index) { const auto block = FindLaidOutArticleBlockByPath(&_blocks, source.path); if (block && block->mediaBlock) { block->mediaBlock->setActiveItemIndex(index); } } MarkdownArticleDropLocation MarkdownArticle::Impl::editDropTarget( QPoint point) const { if (const auto result = hitTest( point, Ui::Text::StateRequest::Flag::LookupSymbol); result.valid() && result.direct && !result.codeHeaderCopy) { if (const auto segment = FindSegment(&_segments, result.segmentIndex)) { if (const auto leaf = EditableLeafForSegment(*segment)) { if (leaf->kind != PreparedEditLeafKind::MathFormula) { auto location = MarkdownArticleDropLocation(); location.target = PreparedEditDropTarget( PreparedEditTextDropTarget{ .leaf = *leaf, .offset = selectionOffsetFromHit( result, TextSelectType::Letters), }); return location; } } } } return EditDropLocationForBlockContainer( _blocks, point, PreparedEditBlockContainerPath(), QRect()); } MarkdownArticleDropLocation MarkdownArticle::Impl::editBlockDropTarget( QPoint point) const { return EditDropLocationForBlockContainer( _blocks, point, PreparedEditBlockContainerPath(), QRect()); } MarkdownArticleDropLocation MarkdownArticle::Impl::editStructuralDropTarget( QPoint point, const PreparedEditSelection &selection) const { if (PointInsideSelectionVerticalSpan(point, _blocks, selection)) { return {}; } switch (selection.kind) { case PreparedEditSelectionKind::Blocks: { if (selection.blocks.empty()) { return {}; } auto best = DropGapCandidate(); ConsiderStructuralBlockDropTargets( &best, _blocks, point, PreparedEditBlockContainerPath(), QRect(), selection); return best.location; } case PreparedEditSelectionKind::ListItems: { if (selection.listItems.empty()) { return {}; } auto best = DropGapCandidate(); ConsiderStructuralListItemDropTargets( &best, _blocks, point, selection.listItems.block, selection); return best.location; } case PreparedEditSelectionKind::TableRows: case PreparedEditSelectionKind::TableCells: case PreparedEditSelectionKind::None: return {}; } return {}; } MarkdownArticleEditControlHit MarkdownArticle::Impl::editControlHitTest( QPoint point) const { return EditControlHitForBlocks(_blocks, point); } int MarkdownArticle::Impl::anchorTop(const QString &anchorId) const { for (const auto &entry : _anchors) { if (entry.first == anchorId) { return entry.second; } } return -1; } auto MarkdownArticle::Impl::scrollAnchorForTop(int top) const -> std::optional { if (_segments.empty()) { return std::nullopt; } const auto make = [&](const SelectableSegment &segment) { const auto rect = segment.outerRect; const auto height = std::max(rect.height(), 1); const auto fraction = std::clamp( (top - rect.top()) / double(height), 0., 1.); return MarkdownArticleScrollAnchor{ segment.index, fraction }; }; const auto span = LookupVisibleSegmentSpan( _segmentTops, _segmentBottoms, { top, top + 1 }); const auto containsVertically = [&](const SelectableSegment &segment) { const auto rect = segment.outerRect; return (rect.top() <= top) && (top < rect.top() + rect.height()); }; auto found = (const SelectableSegment*)nullptr; for (auto i = span.from; i != span.till; ++i) { const auto &segment = _segments[i]; if (!containsVertically(segment)) { continue; } else if (!found || (segment.isTextLeaf() && !found->isTextLeaf())) { found = &segment; } } if (found) { return make(*found); } for (const auto &segment : _segments) { if (segment.outerRect.top() >= top) { return MarkdownArticleScrollAnchor{ segment.index, 0. }; } } return make(_segments.back()); } int MarkdownArticle::Impl::scrollTopForAnchor( const MarkdownArticleScrollAnchor &anchor) const { const auto segment = FindSegment(&_segments, anchor.segmentIndex); if (!segment) { return -1; } const auto rect = segment->outerRect; const auto fraction = std::clamp(anchor.fraction, 0., 1.); return rect.top() + int(std::round(fraction * rect.height())); } MarkdownArticleAnchorExpansion MarkdownArticle::Impl::expandDetailsToAnchor( const QString &anchorId) { const auto result = ExpandDetailsToAnchor( &_content.blocks.blocks, anchorId, false); if (result.changed) { invalidateLayout(); } return result; } MarkdownArticleAnchorExpansion MarkdownArticle::Impl::expandDetailsBlock( const QString &anchorId) { const auto result = ExpandDetailsToAnchor( &_content.blocks.blocks, anchorId, true); if (result.changed) { invalidateLayout(); } return result; } bool MarkdownArticle::Impl::toggleDetails(const QString &anchorId) { if (!ToggleDetailsBlock(&_content.blocks.blocks, anchorId)) { return false; } invalidateLayout(); return true; } bool MarkdownArticle::Impl::segmentIsText(int index) const { const auto segment = FindSegment(&_segments, index); return segment && segment->isTextLeaf(); } bool MarkdownArticle::Impl::segmentIsDisplayMath(int index) const { const auto segment = FindSegment(&_segments, index); return segment && IsDisplayMathSegment(*segment); } bool MarkdownArticle::Impl::segmentIsEditable(int index) const { const auto segment = FindSegment(&_segments, index); return segment && IsEditableSegment(*segment); } int MarkdownArticle::Impl::segmentLength(int index) const { const auto segment = FindSegment(&_segments, index); return segment ? SegmentLength(*segment) : 0; } int MarkdownArticle::Impl::firstTextSegmentIndex() const { for (const auto &segment : _segments) { if (segment.isTextLeaf()) { return segment.index; } } return -1; } int MarkdownArticle::Impl::firstEditableSegmentIndex() const { for (const auto &segment : _segments) { if (IsEditableSegment(segment)) { return segment.index; } } return -1; } int MarkdownArticle::Impl::textLeafIndexForSegment(int segmentIndex) const { auto textLeafIndex = 0; for (const auto &segment : _segments) { if (!segment.isTextLeaf()) { continue; } else if (segment.index == segmentIndex) { return textLeafIndex; } ++textLeafIndex; } return -1; } int MarkdownArticle::Impl::segmentIndexForTextLeafIndex( int textLeafIndex) const { if (textLeafIndex < 0) { return -1; } auto current = 0; for (const auto &segment : _segments) { if (!segment.isTextLeaf()) { continue; } else if (current == textLeafIndex) { return segment.index; } ++current; } return -1; } int MarkdownArticle::Impl::editableIndexForSegment(int segmentIndex) const { auto editableIndex = 0; for (const auto &segment : _segments) { if (!IsEditableSegment(segment)) { continue; } else if (segment.index == segmentIndex) { return editableIndex; } ++editableIndex; } return -1; } int MarkdownArticle::Impl::segmentIndexForEditableIndex( int editableIndex) const { if (editableIndex < 0) { return -1; } auto current = 0; for (const auto &segment : _segments) { if (!IsEditableSegment(segment)) { continue; } else if (current == editableIndex) { return segment.index; } ++current; } return -1; } std::optional MarkdownArticle::Impl::editableLeafForSegment(int segmentIndex) const { const auto segment = FindSegment(&_segments, segmentIndex); return (segment && IsEditableSegment(*segment)) ? EditableLeafForSegment(*segment) : std::nullopt; } int MarkdownArticle::Impl::segmentIndexForEditableLeaf( const PreparedEditLeafSource &source) const { for (const auto &segment : _segments) { if (!IsEditableSegment(segment)) { continue; } const auto leaf = EditableLeafForSegment(segment); if (leaf && (*leaf == source)) { return segment.index; } } return -1; } QRect MarkdownArticle::Impl::textSegmentRect(int segmentIndex) const { const auto segment = FindSegment(&_segments, segmentIndex); return (segment && segment->isTextLeaf()) ? segment->textRect : QRect(); } QRect MarkdownArticle::Impl::logicalSegmentRect(int segmentIndex) const { const auto segment = FindSegment(&_segments, segmentIndex); if (!segment) { return QRect(); } else if (segment->isTextLeaf()) { return LogicalTextRectForSegment(*segment); } else if (IsDisplayMathSegment(*segment)) { return segment->block ? segment->block->formulaRect : QRect(); } return QRect(); } QRect MarkdownArticle::Impl::segmentRect(int segmentIndex) const { const auto segment = FindSegment(&_segments, segmentIndex); if (!segment) { return QRect(); } else if (segment->isTextLeaf()) { return segment->textRect; } else if (IsDisplayMathSegment(*segment)) { if (!segment->outerRect.isEmpty()) { return segment->outerRect; } return segment->block ? segment->block->formulaRect : QRect(); } return QRect(); } QRect MarkdownArticle::Impl::displayMathEditRect(int segmentIndex) const { const auto segment = FindSegment(&_segments, segmentIndex); if (!segment || !IsDisplayMathSegment(*segment) || !segment->block) { return QRect(); } const auto &block = *segment->block; if (!block.contentRect.isEmpty()) { return block.contentRect; } else if (!block.outer.isEmpty()) { return block.outer; } return block.formulaRect; } QRect MarkdownArticle::Impl::displayMathBlockRect(int segmentIndex) const { const auto segment = FindSegment(&_segments, segmentIndex); if (!segment || !IsDisplayMathSegment(*segment) || !segment->block) { return QRect(); } const auto &block = *segment->block; if (!block.outer.isEmpty()) { return block.outer; } return displayMathEditRect(segmentIndex); } const LaidOutBlock *MarkdownArticle::Impl::pullquoteBlockForEditableLeaf( const PreparedEditLeafSource &source) const { const auto find = [&](const auto &self, const std::vector &blocks, const LaidOutBlock *activePullquote) -> const LaidOutBlock * { for (const auto &block : blocks) { const auto nextPullquote = (block.kind == PreparedBlockKind::Quote) && block.pullquote ? &block : activePullquote; if (block.editLeaf && (*block.editLeaf == source) && nextPullquote) { return nextPullquote; } if (const auto nested = self(self, block.children, nextPullquote)) { return nested; } } return nullptr; }; return find(find, _blocks, nullptr); } int MarkdownArticle::Impl::pullquoteAvailableTextWidthForEditableLeaf( const PreparedEditLeafSource &source) const { const auto block = pullquoteBlockForEditableLeaf(source); if (block) { return block->textWidth; } return 0; } MarkdownArticleTextLeafStyle MarkdownArticle::Impl::textLeafStyleForSegment( int segmentIndex) const { const auto segment = FindSegment(&_segments, segmentIndex); if (!segment || !segment->isTextLeaf()) { return {}; } const auto &st = layoutStyle(); const auto &textStyle = TextStyleForSegment(*segment, st); return { .textStyle = &textStyle, .textColor = TextColorForSegment(*segment, st), .markBg = MarkBgColorForStyle(st), .lineHeight = TextLineHeight(textStyle), .align = segment->align, .italic = segment->block && segment->block->pullquote && !segment->block->quoteAuthor, }; } MarkdownArticleTextLeafStyle MarkdownArticle::Impl::editableStyleForSegment( int segmentIndex) const { const auto segment = FindSegment(&_segments, segmentIndex); if (!segment) { return {}; } else if (segment->isTextLeaf()) { return textLeafStyleForSegment(segmentIndex); } else if (!IsDisplayMathSegment(*segment)) { return {}; } const auto &st = layoutStyle(); return { .textStyle = &st.displayMath.fallbackStyle, .textColor = st.displayMath.fg, .markBg = MarkBgColorForStyle(st), .lineHeight = TextLineHeight(st.displayMath.fallbackStyle), .align = ::style::al_center, }; } int MarkdownArticle::Impl::selectionOffsetFromHit( const MarkdownArticleHitTestResult &result, TextSelectType selectionType) const { const auto segment = FindSegment(&_segments, result.segmentIndex); if (!segment) { return 0; } if (result.forcedOffset >= 0) { return std::clamp(result.forcedOffset, 0, SegmentLength(*segment)); } auto offset = int(result.state.symbol); if (selectionType == TextSelectType::Letters && result.state.afterSymbol) { ++offset; } return std::clamp(offset, 0, SegmentLength(*segment)); } TextSelection MarkdownArticle::Impl::adjustSelection( int segmentIndex, TextSelection selection, TextSelectType selectionType) const { const auto segment = FindSegment(&_segments, segmentIndex); if (!segment || !segment->isTextLeaf()) { return selection; } return segment->leaf->adjustSelection(selection, selectionType); } bool MarkdownArticle::Impl::selectionContains( MarkdownArticleSelection selection, const MarkdownArticleSelectionEndpoints *endpoints, const MarkdownArticleHitTestResult &result) const { if (result.codeHeaderCopy) { return false; } const auto segment = FindSegment(&_segments, result.segmentIndex); if (!segment || selection.empty() || !result.valid()) { return false; } const auto selectionState = PaintSelectionState{ .segments = &_segments, .selection = selection, .endpoints = endpoints, }; if (segment->tableSegmentIndex >= 0 && TableSegmentSelected(selectionState, segment->tableSegmentIndex)) { return true; } if (!segment->isTextLeaf()) { return WholeSegmentSelected(*segment, selectionState); } const auto textSelection = TextSelectionForSegment(*segment, selectionState); if (!textSelection || textSelection->empty()) { return false; } const auto offset = selectionOffsetFromHit(result, TextSelectType::Letters); return (offset >= textSelection->from) && (offset < textSelection->to); } TextForMimeData MarkdownArticle::Impl::textForContext( const MarkdownArticleHitTestResult &result) const { if (!result.valid() || !result.direct) { return TextForMimeData(); } const auto segment = FindSegment(&_segments, result.segmentIndex); return segment ? TextForSegment(*segment) : TextForMimeData(); } TextForMimeData MarkdownArticle::Impl::textForSelection( MarkdownArticleSelection selection, const MarkdownArticleSelectionEndpoints *endpoints, const PreparedEditSelection *structuralSelection) const { return TextForSelectedSegments( _segments, selection, endpoints, structuralSelection); } std::vector MarkdownArticle::Impl::richPageSliceForSelection( MarkdownArticleSelection selection) const { if (!_content.richPage) { return {}; } return RichPageBlocksForSelectedSegments( *_content.richPage, _segments, selection); } bool MarkdownArticle::Impl::highlightProcessDone( Spellchecker::HighlightProcessId processId) { const auto i = _pendingHighlightEntries.find(processId); if (i == end(_pendingHighlightEntries)) { return false; } auto entry = std::move(i->second); _pendingHighlightEntries.erase(i); _pendingHighlightProcesses.erase(entry.key); auto rebuilt = false; for (const auto block : entry.blocks) { RepopulateCodeBlockLeaf( *block, &_content.formulas, _inlineFormulaObjects.get(), _content.mediaRuntime, layoutStyle(), true, this, _textRepaint, _textRepaintRect, _textSpoilerLinkFilter); registerPendingHighlightBlock(*block); rebuilt = true; } return rebuilt; } void MarkdownArticle::Impl::invalidatePaletteCache() { InvalidateInlineFormulaPaletteCache(_inlineFormulaObjects); ClearColorizedFormulaImages(&_blocks); } void MarkdownArticle::Impl::invalidateRasterCache() { resetFormulaRasterCache(); InvalidateInlineFormulaRasterCache(_inlineFormulaObjects); ClearColorizedFormulaImages(&_blocks); } bool MarkdownArticle::Impl::hasHeavyPart() const { for (const auto &entry : _mediaBlocks) { const auto &block = entry.second; if (block && block->hasHeavyPart()) { return true; } } return false; } void MarkdownArticle::Impl::unloadHeavyPart() { for (const auto &entry : _mediaBlocks) { if (const auto &block = entry.second) { block->unloadHeavyPart(); } } } void MarkdownArticle::Impl::hideSpoilers() { HideBlocksSpoilers(&_blocks); HideBlocksSpoilers(&_retainedBlocks); for (auto &entry : _cachedTextLeafs.entries) { HideLeafSpoilers(&entry.second.leaf); } for (const auto &entry : _mediaBlocks) { if (const auto &block = entry.second) { block->hideSpoilers(); } } } MediaBlockHost *MarkdownArticle::Impl::mediaBlockHost() const { return _mediaBlockHost; } void MarkdownArticle::Impl::setPlaceholderLoading( PreparedPlaceholderBlockId id) { setPlaceholderLoadingValue(id, true); } void MarkdownArticle::Impl::clearPlaceholderLoading( PreparedPlaceholderBlockId id) { setPlaceholderLoadingValue(id, false); } void MarkdownArticle::Impl::clearAllPlaceholderLoading() { auto repaintIds = std::vector(); repaintIds.reserve(_placeholderRuntimes.size()); for (const auto &[value, runtime] : _placeholderRuntimes) { if (!runtime || !runtime->loading) { continue; } runtime->loading = false; runtime->loadingAnimation.stop(anim::type::instant); repaintIds.push_back({ .value = value }); } for (const auto id : repaintIds) { requestPlaceholderRepaint(id); } } void MarkdownArticle::Impl::addTaskMarkerRipple( const PreparedEditListItemSource &source, QPoint point) { auto block = FindListItemBlock(&_blocks, source); if (!block || block->taskState == TaskState::None || block->markerRect.isEmpty()) { return; } auto runtime = block->taskMarkerRippleRuntime ? block->taskMarkerRippleRuntime : getOrCreateTaskMarkerRippleRuntime(source); if (!runtime) { return; } block->taskMarkerRippleRuntime = runtime; auto view = Ui::CheckView( layoutStyle().list.taskCheck, block->taskState == TaskState::Checked); const auto mask = view.prepareRippleMask(); const auto size = mask.size(); if (!runtime->ripple || runtime->rippleSize != size) { runtime->ripple = std::make_unique( st::defaultCheckbox.ripple, mask, [repaint = runtime->repaint] { if (repaint) { repaint(); } }); runtime->rippleSize = size; } point -= (block->markerRect.topLeft() + st::defaultCheckbox.rippleAreaPosition); point.setX(std::clamp(point.x(), 0, std::max(size.width() - 1, 0))); point.setY(std::clamp(point.y(), 0, std::max(size.height() - 1, 0))); runtime->ripple->add(point); runtime->ripple->lastStop(); if (runtime->repaint) { runtime->repaint(); } } void MarkdownArticle::Impl::addPlaceholderRipple( PreparedPlaceholderBlockId id, QPoint point) { const auto block = FindPlaceholderBlock(&_blocks, id); if (!block) { return; } auto runtime = block->placeholderRuntime ? block->placeholderRuntime : getOrCreatePlaceholderRuntime(id); if (!runtime) { return; } block->placeholderRuntime = runtime; const auto size = block->mediaRect.size(); if (!runtime->ripple || runtime->rippleSize != size) { runtime->ripple = std::make_unique( st::defaultRippleAnimation, Ui::RippleAnimation::RoundRectMask( size, layoutStyle().placeholder.radius), [=] { requestPlaceholderRepaint(id); }); runtime->rippleSize = size; } point.setX(std::clamp(point.x(), 0, std::max(size.width() - 1, 0))); point.setY(std::clamp(point.y(), 0, std::max(size.height() - 1, 0))); runtime->ripple->add(point); requestPlaceholderRepaint(id); } void MarkdownArticle::Impl::stopPlaceholderRipple( PreparedPlaceholderBlockId id) { if (!id) { return; } const auto i = _placeholderRuntimes.find(id.value); if (i == end(_placeholderRuntimes) || !i->second || !i->second->ripple) { return; } i->second->ripple->lastStop(); requestPlaceholderRepaint(id); } void MarkdownArticle::Impl::invalidateLayout() { invalidateLayout(true); } void MarkdownArticle::Impl::invalidateGeometry() { _width = -1; _laidOutWidth = 0; _height = 0; captureScrollState(); clearPendingHighlightBlockPointers(); _anchors.clear(); _segments.clear(); _visibleSegmentSpan = {}; _segmentTops.clear(); _segmentBottoms.clear(); } void MarkdownArticle::Impl::invalidateLayout(bool harvestCurrentBlocks) { invalidateGeometry(); if (harvestCurrentBlocks) { RebuildCachedTextLeafs( _content.blocks.blocks, &_blocks, layoutStyle(), &_cachedTextLeafs); } retainBlocks(); } void MarkdownArticle::Impl::retainBlocks() { if (_blocks.empty()) { _blocksPainted = false; return; } if (_blocksPainted) { _retainedBlocks = std::move(_blocks); } _blocks.clear(); _blocksPainted = false; } int MarkdownArticle::Impl::currentDevicePixelRatio() const { return std::max(style::DevicePixelRatio(), 1); } void MarkdownArticle::Impl::rebuildVisibleSegmentLookup() { RebuildVisibleSegmentLookup( _segments, &_segmentTops, &_segmentBottoms); refreshVisibleSegmentSpan(); } void MarkdownArticle::Impl::refreshVisibleSegmentSpan() { _visibleSegmentSpan = _visibleRange ? LookupVisibleSegmentSpan( _segmentTops, _segmentBottoms, *_visibleRange) : SegmentSpan(); } void MarkdownArticle::Impl::clearMediaBlocks() { ClearMediaBlockStorage(&_mediaBlocks); } void MarkdownArticle::Impl::clearPlaceholderRuntimes() { _placeholderRuntimes.clear(); } void MarkdownArticle::Impl::refreshMediaBlockHosts() { for (const auto &[id, block] : _mediaBlocks) { if (block) { block->setLayoutStyle(layoutStyle()); block->setHost(_mediaBlockHost); } } } auto MarkdownArticle::Impl::getOrCreateTaskMarkerRippleRuntime( const PreparedEditListItemSource &source) -> std::shared_ptr { if (const auto i = _taskMarkerRippleRuntimes.find(source); i != end(_taskMarkerRippleRuntimes)) { return i->second; } auto runtime = std::make_shared([=] { requestTaskMarkerRepaint(source); }); _taskMarkerRippleRuntimes.emplace(source, runtime); return runtime; } std::shared_ptr MarkdownArticle::Impl::getOrCreatePlaceholderRuntime( PreparedPlaceholderBlockId id) { if (!id) { return nullptr; } if (const auto i = _placeholderRuntimes.find(id.value); i != end(_placeholderRuntimes)) { return i->second; } auto runtime = std::make_shared([=] { requestPlaceholderRepaint(id); }); _placeholderRuntimes.emplace(id.value, runtime); return runtime; } void MarkdownArticle::Impl::pruneTaskMarkerRuntimes() { auto live = TaskMarkerSourceSet(); CollectTaskMarkerSources(_blocks, &live); for (auto i = _taskMarkerRippleRuntimes.begin(); i != _taskMarkerRippleRuntimes.end();) { if (live.find(i->first) != end(live)) { ++i; } else { i = _taskMarkerRippleRuntimes.erase(i); } } } void MarkdownArticle::Impl::prunePlaceholderRuntimes() { auto live = std::unordered_set(); CollectPlaceholderIds(_blocks, &live); for (auto i = _placeholderRuntimes.begin(); i != _placeholderRuntimes.end();) { if (live.find(i->first) != end(live)) { ++i; } else { i = _placeholderRuntimes.erase(i); } } } void MarkdownArticle::Impl::requestTaskMarkerRepaint( const PreparedEditListItemSource &source) { if (const auto block = FindListItemBlock(_blocks, source)) { const auto rect = TaskMarkerRippleRect(*block, layoutStyle()); if (_textRepaintRect && !rect.isEmpty()) { _textRepaintRect(rect); } else if (_textRepaint) { _textRepaint(); } } else if (_textRepaint) { _textRepaint(); } } void MarkdownArticle::Impl::requestPlaceholderRepaint( PreparedPlaceholderBlockId id) { if (const auto block = FindPlaceholderBlock(_blocks, id)) { if (_textRepaintRect) { _textRepaintRect(block->mediaRect); } else if (_textRepaint) { _textRepaint(); } } else if (_textRepaint) { _textRepaint(); } } std::shared_ptr MarkdownArticle::Impl::getOrCreateMediaBlock( const PreparedBlock &prepared) { switch (prepared.kind) { case PreparedBlockKind::Photo: return getOrCreateMediaBlock( prepared.photo.id, [=] { return CreatePhotoMediaBlock( prepared.photo, _content.mediaRuntime, layoutStyle()); }); case PreparedBlockKind::Video: return getOrCreateMediaBlock( prepared.video.id, [=] { return CreateVideoMediaBlock( prepared.video, _content.mediaRuntime, layoutStyle()); }); case PreparedBlockKind::Map: return getOrCreateMediaBlock( prepared.map.id, [=] { return CreateMapMediaBlock( prepared.map, _content.mediaRuntime, layoutStyle()); }); case PreparedBlockKind::Audio: return getOrCreateMediaBlock( prepared.audio.id, [=] { return CreateAudioMediaBlock( prepared.audio, _content.mediaRuntime, layoutStyle()); }); case PreparedBlockKind::Channel: return getOrCreateMediaBlock( prepared.channel.id, [=] { return CreateChannelMediaBlock( prepared.channel, _content.mediaRuntime, layoutStyle()); }); case PreparedBlockKind::GroupedMedia: return getOrCreateMediaBlock( prepared.groupedMedia.id, [=] { return CreateGroupedMediaBlock( prepared.groupedMedia, _content.mediaRuntime, layoutStyle()); }); default: return nullptr; } } template std::shared_ptr MarkdownArticle::Impl::getOrCreateMediaBlock( PreparedMediaBlockId id, Factory &&factory) { if (!id) { return nullptr; } if (const auto i = _mediaBlocks.find(id.value); i != end(_mediaBlocks)) { if (i->second && !i->second->alive()) { i->second->setHost(nullptr); _mediaBlocks.erase(i); } else { if (i->second) { i->second->setLayoutStyle(layoutStyle()); i->second->setHost(_mediaBlockHost); } return i->second; } } auto block = factory(); if (block) { block->setLayoutStyle(layoutStyle()); block->setHost(_mediaBlockHost); _mediaBlocks.emplace(id.value, block); } return block; } Spellchecker::HighlightProcessId MarkdownArticle::Impl::tryHighlightSyntax( const QString &displayText, const QString &language, TextWithEntities &marked) { const auto key = PendingHighlightKey{ .text = displayText, .language = language, }; if (const auto i = _pendingHighlightProcesses.find(key); i != end(_pendingHighlightProcesses)) { return i->second; } const auto processId = Spellchecker::TryHighlightSyntax(marked); if (processId) { registerPendingHighlightProcess(key, processId); } return processId; } SegmentSpan MarkdownArticle::Impl::candidateSegmentSpan(QPoint point) const { if (_visibleRange && (_visibleRange->top <= point.y()) && (point.y() < _visibleRange->bottom)) { return _visibleSegmentSpan.empty() ? FullSegmentSpan(_segments) : _visibleSegmentSpan; } return FullSegmentSpan(_segments); } void MarkdownArticle::Impl::clearPendingHighlightBlockPointers() { for (auto &entry : _pendingHighlightEntries) { entry.second.blocks.clear(); } } void MarkdownArticle::Impl::prunePendingHighlightProcessesForContent() { if (_pendingHighlightProcesses.empty()) { return; } auto live = std::unordered_set< PendingHighlightKey, PendingHighlightKeyHasher>(); CollectCodeBlockHighlightKeys(_content.blocks.blocks, &live); for (auto i = _pendingHighlightProcesses.begin(); i != end(_pendingHighlightProcesses);) { if (live.contains(i->first)) { ++i; continue; } _pendingHighlightEntries.erase(i->second); i = _pendingHighlightProcesses.erase(i); } } void MarkdownArticle::Impl::registerPendingHighlightProcess( const PendingHighlightKey &key, Spellchecker::HighlightProcessId processId) { _pendingHighlightProcesses[key] = processId; auto &entry = _pendingHighlightEntries[processId]; entry.key = key; } void MarkdownArticle::Impl::registerPendingHighlightBlock(LaidOutBlock &block) { if (!block.syntaxHighlightProcessId) { return; } if (!_pendingHighlightEntries.contains(block.syntaxHighlightProcessId)) { registerPendingHighlightProcess( PendingHighlightKeyForBlock(block), block.syntaxHighlightProcessId); } _pendingHighlightEntries[block.syntaxHighlightProcessId].blocks.push_back( &block); } void MarkdownArticle::Impl::registerPendingHighlightBlocks(std::vector &blocks) { for (auto &block : blocks) { registerPendingHighlightBlock(block); registerPendingHighlightBlocks(block.children); } } void MarkdownArticle::Impl::resetFormulaRasterCache() { _formulaRenders.clear(); _formulaRenders.resize(_content.formulas.size()); } void MarkdownArticle::Impl::setPlaceholderLoadingValue( PreparedPlaceholderBlockId id, bool loading) { if (!id) { return; } const auto runtime = loading ? getOrCreatePlaceholderRuntime(id) : [&]() -> std::shared_ptr { if (const auto i = _placeholderRuntimes.find(id.value); i != end(_placeholderRuntimes)) { return i->second; } return nullptr; }(); if (!runtime || runtime->loading == loading) { return; } runtime->loading = loading; if (loading) { runtime->loadingAnimation.start(); } else { runtime->loadingAnimation.stop(anim::type::instant); } requestPlaceholderRepaint(id); } const style::Markdown &MarkdownArticle::Impl::layoutStyle() const { return _style; } MarkdownArticleScrollOwnerIdentity MarkdownArticle::Impl::scrollOwnerIdentity( const LaidOutBlock &block, const std::vector &preparedPath) const { if (block.editBlock && ValidBlockPath(block.editBlock->path)) { return { .blockPath = block.editBlock->path }; } return { .preparedPath = preparedPath }; } MarkdownArticleHorizontalScrollLookup MarkdownArticle::Impl::findHorizontalScrollOwner(QPoint point) const { auto preparedPath = std::vector(); return findHorizontalScrollOwner( _blocks, &_content.blocks.blocks, point, &preparedPath); } MarkdownArticleHorizontalScrollLookup MarkdownArticle::Impl::findHorizontalScrollOwner( const std::vector &blocks, const std::vector *prepared, QPoint point, std::vector *preparedPath) const { for (auto i = 0, count = int(blocks.size()); i != count; ++i) { const auto &block = blocks[i]; preparedPath->push_back(i); const auto preparedBlock = (prepared && (i < prepared->size())) ? &(*prepared)[i] : nullptr; if (const auto result = findHorizontalScrollOwner( block.children, preparedBlock ? &preparedBlock->children : nullptr, point, preparedPath); result.block) { return result; } if (block.horizontalScrollMax > 0 && !block.scrollViewportRect.isEmpty()) { const auto identity = scrollOwnerIdentity(block, *preparedPath); auto hit = MarkdownArticleHorizontalScrollHit{ .scrollable = true, .overViewport = ContainsPoint(block.scrollViewportRect, point), .overScrollbar = ContainsPoint( block.scrollScrollbarTrackRect, point), .overScrollbarThumb = ContainsPoint( block.scrollScrollbarThumbRect, point), }; if (hit.overViewport || hit.overScrollbar) { return { .hit = hit, .identity = identity, .block = &block, }; } } preparedPath->pop_back(); } return {}; } std::optional MarkdownArticle::Impl::findHorizontalScrollOwner(int segmentIndex) const { const auto segment = FindSegment(&_segments, segmentIndex); if (!segment || !IsEditableSegment(*segment)) { return std::nullopt; } auto preparedPath = std::vector(); return findHorizontalScrollOwner( _blocks, &_content.blocks.blocks, *segment, std::nullopt, &preparedPath); } std::optional MarkdownArticle::Impl::findHorizontalScrollOwner( const std::vector &blocks, const std::vector *prepared, const SelectableSegment &segment, std::optional owner, std::vector *preparedPath) const { for (auto i = 0, count = int(blocks.size()); i != count; ++i) { const auto &block = blocks[i]; preparedPath->push_back(i); const auto preparedBlock = (prepared && (i < prepared->size())) ? &(*prepared)[i] : nullptr; auto nextOwner = owner; if (!block.scrollViewportRect.isEmpty()) { nextOwner = MarkdownArticleHorizontalScrollLookup{ .identity = scrollOwnerIdentity(block, *preparedPath), .block = &block, }; } const auto matchesCell = [&] { if (!segment.cell) { return false; } for (const auto &row : block.tableRows) { for (const auto &cell : row.cells) { if (segment.cell == &cell) { return true; } } } return false; }(); if (segment.block == &block || matchesCell) { return nextOwner; } if (const auto result = findHorizontalScrollOwner( block.children, preparedBlock ? &preparedBlock->children : nullptr, segment, nextOwner, preparedPath)) { return result; } preparedPath->pop_back(); } return std::nullopt; } LaidOutBlock *MarkdownArticle::Impl::findScrollOwnerByIdentity( const MarkdownArticleScrollOwnerIdentity &identity) { auto preparedPath = std::vector(); return findScrollOwnerByIdentity( _blocks, &_content.blocks.blocks, identity, &preparedPath); } LaidOutBlock *MarkdownArticle::Impl::findScrollOwnerByIdentity( std::vector &blocks, const std::vector *prepared, const MarkdownArticleScrollOwnerIdentity &identity, std::vector *preparedPath) { for (auto i = 0, count = int(blocks.size()); i != count; ++i) { auto &block = blocks[i]; preparedPath->push_back(i); const auto preparedBlock = (prepared && (i < prepared->size())) ? &(*prepared)[i] : nullptr; if (!block.scrollViewportRect.isEmpty()) { const auto current = scrollOwnerIdentity(block, *preparedPath); if (current == identity) { return █ } } if (const auto child = findScrollOwnerByIdentity( block.children, preparedBlock ? &preparedBlock->children : nullptr, identity, preparedPath)) { return child; } preparedPath->pop_back(); } return nullptr; } void MarkdownArticle::Impl::captureScrollState() { auto preparedPath = std::vector(); captureScrollState( _blocks, &_content.blocks.blocks, &preparedPath); } void MarkdownArticle::Impl::captureScrollState( const std::vector &blocks, const std::vector *prepared, std::vector *preparedPath) { for (auto i = 0, count = int(blocks.size()); i != count; ++i) { const auto &block = blocks[i]; preparedPath->push_back(i); const auto preparedBlock = (prepared && (i < prepared->size())) ? &(*prepared)[i] : nullptr; if (!block.scrollViewportRect.isEmpty()) { const auto identity = scrollOwnerIdentity(block, *preparedPath); if (block.horizontalScrollLeft > 0) { _capturedScrollLefts[identity] = block.horizontalScrollLeft; } else { _capturedScrollLefts.erase(identity); } } captureScrollState( block.children, preparedBlock ? &preparedBlock->children : nullptr, preparedPath); preparedPath->pop_back(); } } void MarkdownArticle::Impl::restoreScrollState() { auto preparedPath = std::vector(); restoreScrollState( _blocks, &_content.blocks.blocks, &preparedPath); } void MarkdownArticle::Impl::restoreScrollState( std::vector &blocks, const std::vector *prepared, std::vector *preparedPath) { for (auto i = 0, count = int(blocks.size()); i != count; ++i) { auto &block = blocks[i]; preparedPath->push_back(i); const auto preparedBlock = (prepared && (i < prepared->size())) ? &(*prepared)[i] : nullptr; if (!block.scrollViewportRect.isEmpty()) { const auto identity = scrollOwnerIdentity(block, *preparedPath); const auto j = _capturedScrollLefts.find(identity); block.horizontalScrollLeft = (j != end(_capturedScrollLefts)) ? std::clamp(j->second, 0, block.horizontalScrollMax) : 0; } restoreScrollState( block.children, preparedBlock ? &preparedBlock->children : nullptr, preparedPath); preparedPath->pop_back(); } } void MarkdownArticle::Impl::refreshScrolledGeometry( LaidOutBlock &block, const ActiveHorizontalScrollOwnerState *activeOwner) { RestoreLogicalBlockGeometry(&block); if (activeOwner) { ApplyTranslatedDescendantGeometry(&block, *activeOwner); } std::optional nextOwner; if (!activeOwner && !block.scrollViewportRect.isEmpty()) { block.horizontalScrollLeft = std::clamp( block.horizontalScrollLeft, 0, block.horizontalScrollMax); if (block.horizontalScrollMax > 0) { const auto owner = ActiveHorizontalScrollOwnerState{ .viewport = block.scrollViewportRect, .shift = -block.horizontalScrollLeft, }; if (ScrollOwnerMovesOwnContent(block.kind)) { ApplyOwnerContentGeometry(&block, owner); } if (ScrollOwnerMovesChildren(block.kind)) { nextOwner = owner; } } updateScrollbarThumb(block); } refreshScrolledGeometry( block.children, nextOwner ? &*nextOwner : activeOwner); } void MarkdownArticle::Impl::refreshScrolledGeometry(LaidOutBlock &block) { refreshScrolledGeometry(block, nullptr); } void MarkdownArticle::Impl::refreshScrolledGeometry( std::vector &blocks) { refreshScrolledGeometry(blocks, nullptr); } void MarkdownArticle::Impl::refreshScrolledGeometry( std::vector &blocks, const ActiveHorizontalScrollOwnerState *activeOwner) { for (auto &block : blocks) { refreshScrolledGeometry(block, activeOwner); } } void MarkdownArticle::Impl::updateScrollbarThumb(LaidOutBlock &block) { if (block.horizontalScrollMax <= 0 || block.scrollScrollbarTrackRect.isEmpty()) { block.scrollScrollbarThumbRect = QRect(); block.tableScrollbarThumbRect = QRect(); return; } const auto trackWidth = block.scrollScrollbarTrackRect.width(); if (trackWidth <= 0) { block.scrollScrollbarThumbRect = QRect(); block.tableScrollbarThumbRect = QRect(); return; } const auto viewportWidth = std::max(block.scrollViewportRect.width(), 1); const auto contentWidth = std::max(block.scrollLogicalContentRect.width(), 1); auto thumbWidth = (trackWidth * viewportWidth + (contentWidth / 2)) / contentWidth; thumbWidth = std::clamp( thumbWidth, std::min(layoutStyle().table.scrollbarMinThumbWidth, trackWidth), trackWidth); const auto available = std::max(trackWidth - thumbWidth, 0); const auto thumbOffset = (available > 0) ? ((block.horizontalScrollLeft * available) + (block.horizontalScrollMax / 2)) / block.horizontalScrollMax : 0; block.scrollScrollbarThumbRect = QRect( block.scrollScrollbarTrackRect.x() + thumbOffset, block.scrollScrollbarTrackRect.y(), thumbWidth, block.scrollScrollbarTrackRect.height()); if (block.kind == PreparedBlockKind::Table) { block.tableScrollbarThumbRect = block.scrollScrollbarThumbRect; } } bool MarkdownArticle::Impl::setScrollLeft( LaidOutBlock &block, const MarkdownArticleScrollOwnerIdentity &identity, int left) { left = std::clamp(left, 0, block.horizontalScrollMax); if (block.horizontalScrollLeft == left) { return false; } block.horizontalScrollLeft = left; if (left > 0) { _capturedScrollLefts[identity] = left; } else { _capturedScrollLefts.erase(identity); } refreshScrolledGeometry(block); RefreshScrollableSegmentRects(block, &_segments); if (_textRepaintRect) { _textRepaintRect(block.outer); } else if (_textRepaint) { _textRepaint(); } return true; } bool MarkdownArticle::Impl::revealSegment(int segmentIndex) { const auto logicalRect = logicalSegmentRect(segmentIndex); if (logicalRect.isEmpty()) { return false; } const auto lookup = findHorizontalScrollOwner(segmentIndex); if (!lookup || !lookup->block) { return false; } const auto &owner = *lookup->block; if (owner.scrollViewportRect.isEmpty() || owner.scrollLogicalContentRect.isEmpty()) { return false; } const auto viewportWidth = owner.scrollViewportRect.width(); if (viewportWidth <= 0) { return false; } const auto toLeft = logicalRect.x() + owner.horizontalScrollLeft - owner.scrollLogicalContentRect.x(); const auto left = ComputeScrollTo( toLeft, toLeft + logicalRect.width(), 0, owner.horizontalScrollMax, owner.horizontalScrollLeft, viewportWidth); if (left == owner.horizontalScrollLeft) { return false; } if (const auto block = findScrollOwnerByIdentity(lookup->identity)) { return setScrollLeft(*block, lookup->identity, left); } return false; } MarkdownArticleHorizontalScrollHit MarkdownArticle::Impl::horizontalScrollHit( QPoint point) const { return findHorizontalScrollOwner(point).hit; } bool MarkdownArticle::Impl::canConsumeHorizontalScroll( QPoint point, int delta) const { if (const auto lookup = findHorizontalScrollOwner(point); lookup.block) { const auto left = std::clamp( lookup.block->horizontalScrollLeft - delta, 0, lookup.block->horizontalScrollMax); return (left != lookup.block->horizontalScrollLeft); } return false; } bool MarkdownArticle::Impl::consumeHorizontalScroll(QPoint point, int delta) { if (const auto lookup = findHorizontalScrollOwner(point); lookup.block) { if (const auto block = findScrollOwnerByIdentity(lookup.identity)) { return setScrollLeft( *block, lookup.identity, block->horizontalScrollLeft - delta); } } return false; } bool MarkdownArticle::Impl::beginHorizontalScroll( QPoint point, bool fromTouch) { const auto lookup = findHorizontalScrollOwner(point); if (!lookup.block) { return false; } if (fromTouch) { if (!lookup.hit.overViewport) { return false; } _activeHorizontalScrollDrag = ActiveHorizontalScrollDrag{ .owner = lookup.identity, .pressPoint = point, .startScrollLeft = lookup.block->horizontalScrollLeft, .fromTouch = true, }; return true; } if (!lookup.hit.overScrollbar) { return false; } const auto &thumb = lookup.block->scrollScrollbarThumbRect; _activeHorizontalScrollDrag = ActiveHorizontalScrollDrag{ .owner = lookup.identity, .pressPoint = point, .startScrollLeft = lookup.block->horizontalScrollLeft, .thumbGrabOffset = lookup.hit.overScrollbarThumb ? (point.x() - thumb.x()) : (thumb.width() / 2), }; if (!lookup.hit.overScrollbarThumb) { (void)updateHorizontalScroll(point); } return true; } bool MarkdownArticle::Impl::updateHorizontalScroll(QPoint point) { if (!_activeHorizontalScrollDrag) { return false; } const auto drag = *_activeHorizontalScrollDrag; const auto block = findScrollOwnerByIdentity(drag.owner); if (!block) { return false; } if (drag.fromTouch) { return setScrollLeft( *block, drag.owner, drag.startScrollLeft - (point.x() - drag.pressPoint.x())); } if (block->scrollScrollbarTrackRect.isEmpty()) { return false; } const auto available = std::max( block->scrollScrollbarTrackRect.width() - block->scrollScrollbarThumbRect.width(), 0); auto thumbLeft = point.x() - drag.thumbGrabOffset; thumbLeft = std::clamp( thumbLeft, block->scrollScrollbarTrackRect.x(), block->scrollScrollbarTrackRect.x() + available); const auto left = (available > 0) ? (((thumbLeft - block->scrollScrollbarTrackRect.x()) * block->horizontalScrollMax) + (available / 2)) / available : 0; return setScrollLeft(*block, drag.owner, left); } void MarkdownArticle::Impl::endHorizontalScroll() { _activeHorizontalScrollDrag.reset(); } // The laid out width is passed through the _width field, assigned by the // callers right before the call, instead of a parameter, because GCC 15 // IPA-CP with LTO wrongly constant-folded such a parameter to 1 (the lower // bound of the std::max(width, 1) clamps in the callers), collapsing rich // message bubbles to the minimum width in release Linux builds. void MarkdownArticle::Impl::finalizeRelayout(int heightBottom) { const auto &page = layoutStyle().pagePadding; ++_layoutGeneration; restoreScrollState(); refreshScrolledGeometry(_blocks); _laidOutWidth = std::min( _width, std::max( ArticleContentMaxRight(_blocks, layoutStyle()) + page.right(), page.left() + page.right() + 1)); pruneTaskMarkerRuntimes(); prunePlaceholderRuntimes(); _relatedArticleImages.clear(); StoreRelatedArticleImageStates( _blocks, &_relatedArticleImages); _height = heightBottom + page.bottom(); clearPendingHighlightBlockPointers(); _anchors.clear(); _segments.clear(); _visibleSegmentSpan = {}; _segmentTops.clear(); _segmentBottoms.clear(); registerPendingHighlightBlocks(_blocks); CollectAnchors(_blocks, &_anchors); CollectSelectableSegments(&_blocks, &_segments); RefreshScrollableSegmentRects(_blocks, &_segments); rebuildVisibleSegmentLookup(); } void MarkdownArticle::Impl::relayout(int width) { width = std::max(width, 1); if (_width == width) { return; } StoreRelatedArticleImageStates( _blocks, &_relatedArticleImages); invalidateGeometry(); RebuildCachedTextLeafs( _content.blocks.blocks, &_blocks, layoutStyle(), &_cachedTextLeafs); retainBlocks(); _missingMediaBlocks = 0; const auto &st = layoutStyle(); const auto &page = st.pagePadding; const auto innerWidth = std::max(width - page.left() - page.right(), 1); auto context = LayoutContext{ .articleLeft = page.left(), .articleWidth = innerWidth, .mediaPixelScale = _mediaPixelScale, .useArticleBands = true, .editMode = _content.editMode, .syntaxHighlightTracker = this, .cachedTextLeafs = &_cachedTextLeafs, .repaint = _textRepaint, .repaintRect = _textRepaintRect, .spoilerLinkFilter = _textSpoilerLinkFilter, }; if (_editableMaxLineWidthOverrideLeaf && (_editableMaxLineWidthOverride > 0)) { context.editableMaxLineWidthOverride = std::make_shared( EditableMaxLineWidthOverride{ .leaf = *_editableMaxLineWidthOverrideLeaf, .width = _editableMaxLineWidthOverride, }); } if (_editableTextEmptyOverrideLeaf) { context.editableTextEmptyOverride = std::make_shared( EditableTextEmptyOverride{ .leaf = *_editableTextEmptyOverrideLeaf, .empty = _editableTextEmptyOverride, }); } if (_editableHeightOverrideIndex >= 0 && _editableHeightOverride > 0) { context.editableHeightOverride = std::make_shared( EditableHeightOverride{ .editableIndex = _editableHeightOverrideIndex, .height = _editableHeightOverride, }); } context.mediaBlockFactory = [=](const PreparedBlock &prepared) { auto block = getOrCreateMediaBlock(prepared); if (!block && _content.mediaRuntime && ExpectsMediaBlock(prepared)) { ++_missingMediaBlocks; } return block; }; context.placeholderRuntimeFactory = [=](PreparedPlaceholderBlockId id) { return getOrCreatePlaceholderRuntime(id); }; context.taskMarkerRippleRuntimeFactory = [=](const PreparedEditListItemSource &source) { return getOrCreateTaskMarkerRippleRuntime(source); }; const auto contextScope = LayoutContextScope(context); (void)contextScope; const auto y = LayoutBlocks( _content.blocks.blocks, &_content.formulas, &_formulaRenders, _renderer.get(), _inlineFormulaObjects.get(), _content.mediaRuntime, &_blocks, st, page.left(), page.top(), innerWidth, context); RestoreRelatedArticleImageStates( &_blocks, _relatedArticleImages); _width = width; finalizeRelayout(y); } void MarkdownArticle::Impl::relayoutRetained(int width) { width = std::max(width, 1); if (_width == width) { return; } else if (_blocks.empty()) { relayout(width); return; } captureScrollState(); const auto &st = layoutStyle(); const auto &page = st.pagePadding; const auto innerWidth = std::max(width - page.left() - page.right(), 1); auto context = LayoutContext{ .articleLeft = page.left(), .articleWidth = innerWidth, .mediaPixelScale = _mediaPixelScale, .useArticleBands = true, .editMode = _content.editMode, .syntaxHighlightTracker = this, .cachedTextLeafs = &_cachedTextLeafs, .repaint = _textRepaint, .repaintRect = _textRepaintRect, .spoilerLinkFilter = _textSpoilerLinkFilter, }; if (_editableMaxLineWidthOverrideLeaf && (_editableMaxLineWidthOverride > 0)) { context.editableMaxLineWidthOverride = std::make_shared( EditableMaxLineWidthOverride{ .leaf = *_editableMaxLineWidthOverrideLeaf, .width = _editableMaxLineWidthOverride, }); } if (_editableTextEmptyOverrideLeaf) { context.editableTextEmptyOverride = std::make_shared( EditableTextEmptyOverride{ .leaf = *_editableTextEmptyOverrideLeaf, .empty = _editableTextEmptyOverride, }); } if (_editableHeightOverrideIndex >= 0 && _editableHeightOverride > 0) { context.editableHeightOverride = std::make_shared( EditableHeightOverride{ .editableIndex = _editableHeightOverrideIndex, .height = _editableHeightOverride, }); } context.mediaBlockFactory = [=](const PreparedBlock &prepared) { return getOrCreateMediaBlock(prepared); }; context.placeholderRuntimeFactory = [=](PreparedPlaceholderBlockId id) { return getOrCreatePlaceholderRuntime(id); }; context.taskMarkerRippleRuntimeFactory = [=](const PreparedEditListItemSource &source) { return getOrCreateTaskMarkerRippleRuntime(source); }; const auto contextScope = LayoutContextScope(context); (void)contextScope; const auto y = RecountLaidOutBlocks( _content.blocks.blocks, _content.formulas, &_blocks, st, page.left(), page.top(), innerWidth, context); if (!y) { relayout(width); return; } _width = width; finalizeRelayout(*y); } MarkdownArticle::MarkdownArticle( const style::Markdown &st, std::shared_ptr renderer) : _impl(std::make_unique(st, std::move(renderer))) { } MarkdownArticle::~MarkdownArticle() = default; MarkdownArticle::MarkdownArticle(MarkdownArticle &&) noexcept = default; MarkdownArticle &MarkdownArticle::operator=(MarkdownArticle &&) noexcept = default; void MarkdownArticle::setRenderer(std::shared_ptr renderer) { _impl->setRenderer(std::move(renderer)); } void MarkdownArticle::setMediaBlockHost(MediaBlockHost *host) { _impl->setMediaBlockHost(host); } void MarkdownArticle::setMediaPixelScale(double scale) { _impl->setMediaPixelScale(scale); } void MarkdownArticle::setTextRepaintCallbacks( Fn repaint, Fn repaintRect, Fn spoilerLinkFilter) { _impl->setTextRepaintCallbacks( std::move(repaint), std::move(repaintRect), std::move(spoilerLinkFilter)); } void MarkdownArticle::setContent(MarkdownArticleContent content) { _impl->setContent(std::move(content)); } void MarkdownArticle::setSearchMatches( std::vector matches, int current) { _impl->setSearchMatches(std::move(matches), current); } auto MarkdownArticle::searchSources() const -> std::vector { return _impl->searchSources(); } void MarkdownArticle::updatePreparedLeaf( const PreparedEditLeafSource &source, const MarkdownArticleContent &prepared) { _impl->updatePreparedLeaf(source, prepared); } void MarkdownArticle::setEditableMaxLineWidthOverride( const PreparedEditLeafSource &source, int width) { _impl->setEditableMaxLineWidthOverride(source, width); } void MarkdownArticle::setEditableTextEmptyOverride( const PreparedEditLeafSource &source, bool empty) { _impl->setEditableTextEmptyOverride(source, empty); } void MarkdownArticle::setEditableHeightOverride( int editableIndex, int height) { _impl->setEditableHeightOverride(editableIndex, height); } void MarkdownArticle::setEditableHeightOverrideForSegment( int segmentIndex, int height) { _impl->setEditableHeightOverrideForSegment(segmentIndex, height); } void MarkdownArticle::clearEditableMaxLineWidthOverride() { _impl->clearEditableMaxLineWidthOverride(); } void MarkdownArticle::clearEditableTextEmptyOverride() { _impl->clearEditableTextEmptyOverride(); } void MarkdownArticle::clearEditableHeightOverride() { _impl->clearEditableHeightOverride(); } void MarkdownArticle::setTextLeafHeightOverride( int textLeafIndex, int height) { _impl->setTextLeafHeightOverride(textLeafIndex, height); } void MarkdownArticle::clearTextLeafHeightOverride() { _impl->clearTextLeafHeightOverride(); } void MarkdownArticle::invalidateLayout() { _impl->invalidateLayout(); } int MarkdownArticle::maxWidth() const { return const_cast(_impl.get())->maxWidth(); } int MarkdownArticle::lastLayoutWidth() const { return _impl->lastLayoutWidth(); } bool MarkdownArticle::hasMissingMediaBlocks() const { return _impl->hasMissingMediaBlocks(); } int MarkdownArticle::resizeGetHeight(int width) { return _impl->resizeGetHeight(width); } auto MarkdownArticle::countRevealLinesGeometry(int width) -> std::vector { return _impl->countRevealLinesGeometry(width); } void MarkdownArticle::setVisibleTopBottom(int visibleTop, int visibleBottom) { _impl->setVisibleTopBottom(visibleTop, visibleBottom); } void MarkdownArticle::paint( Painter &p, const MarkdownArticlePaintContext &context) const { _impl->paint(p, context); } MarkdownArticleHitTestResult MarkdownArticle::hitTest( QPoint point, Ui::Text::StateRequest::Flags flags) const { return _impl->hitTest(point, flags); } PreparedEditHit MarkdownArticle::editHitTest(QPoint point) const { return _impl->editHitTest(point); } MarkdownArticleDropLocation MarkdownArticle::editDropTarget( QPoint point) const { return _impl->editDropTarget(point); } MarkdownArticleDropLocation MarkdownArticle::editBlockDropTarget( QPoint point) const { return _impl->editBlockDropTarget(point); } MarkdownArticleDropLocation MarkdownArticle::editStructuralDropTarget( QPoint point, const PreparedEditSelection &selection) const { return _impl->editStructuralDropTarget(point, selection); } MarkdownArticleEditControlHit MarkdownArticle::editControlHitTest( QPoint point) const { return _impl->editControlHitTest(point); } void MarkdownArticle::addTaskMarkerRipple( const PreparedEditListItemSource &source, QPoint point) { _impl->addTaskMarkerRipple(source, point); } MarkdownArticleHorizontalScrollHit MarkdownArticle::horizontalScrollHit( QPoint point) const { return _impl->horizontalScrollHit(point); } bool MarkdownArticle::canConsumeHorizontalScroll( QPoint point, int delta) const { return _impl->canConsumeHorizontalScroll(point, delta); } bool MarkdownArticle::consumeHorizontalScroll(QPoint point, int delta) { return _impl->consumeHorizontalScroll(point, delta); } bool MarkdownArticle::beginHorizontalScroll(QPoint point, bool fromTouch) { return _impl->beginHorizontalScroll(point, fromTouch); } bool MarkdownArticle::updateHorizontalScroll(QPoint point) { return _impl->updateHorizontalScroll(point); } void MarkdownArticle::endHorizontalScroll() { _impl->endHorizontalScroll(); } int MarkdownArticle::anchorTop(const QString &anchorId) const { return _impl->anchorTop(anchorId); } auto MarkdownArticle::scrollAnchorForTop(int top) const -> std::optional { return _impl->scrollAnchorForTop(top); } int MarkdownArticle::scrollTopForAnchor( const MarkdownArticleScrollAnchor &anchor) const { return _impl->scrollTopForAnchor(anchor); } MarkdownArticleAnchorExpansion MarkdownArticle::expandDetailsToAnchor( const QString &anchorId) { return _impl->expandDetailsToAnchor(anchorId); } MarkdownArticleAnchorExpansion MarkdownArticle::expandDetailsBlock( const QString &anchorId) { return _impl->expandDetailsBlock(anchorId); } bool MarkdownArticle::toggleDetails(const QString &anchorId) { return _impl->toggleDetails(anchorId); } bool MarkdownArticle::segmentIsText(int index) const { return _impl->segmentIsText(index); } bool MarkdownArticle::segmentIsDisplayMath(int index) const { return _impl->segmentIsDisplayMath(index); } bool MarkdownArticle::segmentIsEditable(int index) const { return _impl->segmentIsEditable(index); } int MarkdownArticle::segmentLength(int index) const { return _impl->segmentLength(index); } int MarkdownArticle::firstTextSegmentIndex() const { return _impl->firstTextSegmentIndex(); } int MarkdownArticle::firstEditableSegmentIndex() const { return _impl->firstEditableSegmentIndex(); } int MarkdownArticle::textLeafIndexForSegment(int segmentIndex) const { return _impl->textLeafIndexForSegment(segmentIndex); } int MarkdownArticle::segmentIndexForTextLeafIndex(int textLeafIndex) const { return _impl->segmentIndexForTextLeafIndex(textLeafIndex); } int MarkdownArticle::editableIndexForSegment(int segmentIndex) const { return _impl->editableIndexForSegment(segmentIndex); } int MarkdownArticle::segmentIndexForEditableIndex(int editableIndex) const { return _impl->segmentIndexForEditableIndex(editableIndex); } std::optional MarkdownArticle::editableLeafForSegment( int segmentIndex) const { return _impl->editableLeafForSegment(segmentIndex); } int MarkdownArticle::segmentIndexForEditableLeaf( const PreparedEditLeafSource &source) const { return _impl->segmentIndexForEditableLeaf(source); } QRect MarkdownArticle::textSegmentRect(int segmentIndex) const { return _impl->textSegmentRect(segmentIndex); } QRect MarkdownArticle::logicalSegmentRect(int segmentIndex) const { return _impl->logicalSegmentRect(segmentIndex); } QRect MarkdownArticle::segmentRect(int segmentIndex) const { return _impl->segmentRect(segmentIndex); } std::vector MarkdownArticle::mediaBlockGeometries() const { return _impl->mediaBlockGeometries(); } void MarkdownArticle::setGroupedActiveIndex( const PreparedEditBlockSource &source, int index) { _impl->setGroupedActiveIndex(source, index); } QRect MarkdownArticle::displayMathEditRect(int segmentIndex) const { return _impl->displayMathEditRect(segmentIndex); } QRect MarkdownArticle::displayMathBlockRect(int segmentIndex) const { return _impl->displayMathBlockRect(segmentIndex); } int MarkdownArticle::pullquoteAvailableTextWidthForEditableLeaf( const PreparedEditLeafSource &source) const { return _impl->pullquoteAvailableTextWidthForEditableLeaf(source); } bool MarkdownArticle::revealSegment(int segmentIndex) { return _impl->revealSegment(segmentIndex); } MarkdownArticleTextLeafStyle MarkdownArticle::textLeafStyleForSegment( int segmentIndex) const { return _impl->textLeafStyleForSegment(segmentIndex); } MarkdownArticleTextLeafStyle MarkdownArticle::editableStyleForSegment( int segmentIndex) const { return _impl->editableStyleForSegment(segmentIndex); } int MarkdownArticle::selectionOffsetFromHit( const MarkdownArticleHitTestResult &result, TextSelectType selectionType) const { return _impl->selectionOffsetFromHit(result, selectionType); } TextSelection MarkdownArticle::adjustSelection( int segmentIndex, TextSelection selection, TextSelectType selectionType) const { return _impl->adjustSelection(segmentIndex, selection, selectionType); } bool MarkdownArticle::selectionContains( MarkdownArticleSelection selection, const MarkdownArticleSelectionEndpoints *endpoints, const MarkdownArticleHitTestResult &result) const { return _impl->selectionContains(selection, endpoints, result); } TextForMimeData MarkdownArticle::textForContext( const MarkdownArticleHitTestResult &result) const { return _impl->textForContext(result); } TextForMimeData MarkdownArticle::textForSelection( MarkdownArticleSelection selection, const MarkdownArticleSelectionEndpoints *endpoints, const PreparedEditSelection *structuralSelection) const { return _impl->textForSelection( selection, endpoints, structuralSelection); } std::vector MarkdownArticle::richPageSliceForSelection( MarkdownArticleSelection selection) const { return _impl->richPageSliceForSelection(selection); } bool MarkdownArticle::highlightProcessDone( Spellchecker::HighlightProcessId processId) { return _impl->highlightProcessDone(processId); } void MarkdownArticle::invalidatePaletteCache() { _impl->invalidatePaletteCache(); } void MarkdownArticle::invalidateRasterCache() { _impl->invalidateRasterCache(); } bool MarkdownArticle::hasHeavyPart() const { return _impl && _impl->hasHeavyPart(); } void MarkdownArticle::unloadHeavyPart() { _impl->unloadHeavyPart(); } void MarkdownArticle::hideSpoilers() { _impl->hideSpoilers(); } MediaBlockHost *MarkdownArticle::mediaBlockHost() const { return _impl->mediaBlockHost(); } void MarkdownArticle::setPlaceholderLoading(PreparedPlaceholderBlockId id) { _impl->setPlaceholderLoading(id); } void MarkdownArticle::clearPlaceholderLoading(PreparedPlaceholderBlockId id) { _impl->clearPlaceholderLoading(id); } void MarkdownArticle::clearAllPlaceholderLoading() { _impl->clearAllPlaceholderLoading(); } void MarkdownArticle::addPlaceholderRipple( PreparedPlaceholderBlockId id, QPoint point) { _impl->addPlaceholderRipple(id, point); } void MarkdownArticle::stopPlaceholderRipple(PreparedPlaceholderBlockId id) { _impl->stopPlaceholderRipple(id); } void MarkdownArticle::clearBeforeDestroy() { base::take(_impl); } } // namespace Iv::Markdown