From 67484a0c04cbe5dfd689a79155e6fcef0766c997 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 11 Jun 2026 14:27:37 +0400 Subject: [PATCH] Optimize reveal animation of rich messages. --- .../iv/markdown/iv_markdown_article.cpp | 14 ++++- .../iv/markdown/iv_markdown_article.h | 12 +++++ .../iv_markdown_article_layout_blocks.cpp | 16 ++++-- .../iv_markdown_article_layout_blocks.h | 5 ++ .../iv/markdown/iv_markdown_article_paint.cpp | 39 ++++++++++++-- .../iv_markdown_article_selection.cpp | 53 +++++++++++-------- .../markdown/iv_markdown_article_selection.h | 3 ++ 7 files changed, 110 insertions(+), 32 deletions(-) diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp index 6dd1cc39c4..56479d1aff 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp @@ -2782,6 +2782,8 @@ private: int _width = -1; int _laidOutWidth = 0; int _height = 0; + int _layoutGeneration = 0; + MarkdownArticleRevealLineCountsCache _revealLineCounts; CachedTextLeafPool _cachedTextLeafs; std::vector _blocks; std::vector _retainedBlocks; @@ -3100,6 +3102,15 @@ void MarkdownArticle::Impl::paint( 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( @@ -4354,7 +4365,7 @@ bool MarkdownArticle::Impl::setScrollLeft( _capturedScrollLefts.erase(identity); } refreshScrolledGeometry(block); - RefreshScrollableSegmentRects(_blocks, &_segments); + RefreshScrollableSegmentRects(block, &_segments); if (_textRepaintRect) { _textRepaintRect(block.outer); } else if (_textRepaint) { @@ -4516,6 +4527,7 @@ void MarkdownArticle::Impl::endHorizontalScroll() { // 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( diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h index c8c218a9e6..dee8567380 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "iv/markdown/iv_markdown_media_block.h" #include "iv/markdown/iv_markdown_prepare.h" +#include "base/flat_map.h" #include "spellcheck/spellcheck_highlight_syntax.h" #include "ui/chat/chat_style.h" #include "ui/effects/radial_animation.h" @@ -156,10 +157,21 @@ struct MarkdownArticleRevealPostprocess { not_null cache; }; +// Caches per-block / per-leaf reveal line counts while the reveal +// animation is active, so that painting doesn't re-run line breaking +// for all the blocks above the clip on every frame. Owned by the +// article, valid for a single layout generation, freed on the first +// paint after the reveal finishes. +struct MarkdownArticleRevealLineCountsCache { + int layoutGeneration = -1; + base::flat_map counts; +}; + struct MarkdownArticleRevealPaintState { int activeLine = -1; int nextLine = 0; const MarkdownArticleRevealPostprocess *postprocess = nullptr; + MarkdownArticleRevealLineCountsCache *lineCounts = nullptr; }; struct MarkdownArticlePaintContext final : Ui::ChatPaintContext { diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp index 12ee99dbc3..0be596ff52 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp @@ -2632,6 +2632,8 @@ void UpdateLaidOutLeafContent( cell->rowspan = std::max(prepared.rowspan, 1); cell->placeholderText = QString(); cell->placeholderLeaf = Ui::Text::String(); + cell->cachedPreferredWidth = -1; + cell->cachedPreferredHeight = 0; BuildOrReuseCachedTextLeaf( &cell->leaf, nullptr, @@ -3919,11 +3921,15 @@ LaidOutBlock LayoutGroupedMediaBlock( cellData.usePlaceholder = usePlaceholder; cellData.minimumWidth = LeafMinimumWidth(displayLeaf); cellData.preferredWidth = displayLeaf.maxWidth(); - cellData.preferredHeight = std::max( - displayLeaf.countHeight( - std::max(cellData.preferredWidth, 1), - true), - TextLineHeight(textStyle)); + if (cell.cachedPreferredWidth != cellData.preferredWidth) { + cell.cachedPreferredWidth = cellData.preferredWidth; + cell.cachedPreferredHeight = std::max( + displayLeaf.countHeight( + std::max(cellData.preferredWidth, 1), + true), + TextLineHeight(textStyle)); + } + cellData.preferredHeight = cell.cachedPreferredHeight; row.cells.push_back(std::move(cellData)); } rows.push_back(std::move(row)); diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.h index 8d7cce636c..084da0259d 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.h @@ -48,6 +48,11 @@ struct LaidOutTableCell { QRect outer; QRect textRect; int textWidth = 0; + + // Content-dependent, survives geometry resets, recomputed when + // the displayed leaf max width changes (so on content changes). + int cachedPreferredWidth = -1; + int cachedPreferredHeight = 0; bool header = false; PreparedTableCellVerticalAlignment verticalAlignment = PreparedTableCellVerticalAlignment::Top; diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp index 28eb893a04..6de54fb2d8 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp @@ -329,12 +329,33 @@ void PaintSelectableTextLeaf( return result; } +template +[[nodiscard]] int CachedRevealLineCount( + const MarkdownArticlePaintContext &context, + const void *key, + CountCallback &&count) { + const auto cache = context.reveal ? context.reveal->lineCounts : nullptr; + if (!cache) { + return count(); + } + const auto i = cache->counts.find(key); + if (i != end(cache->counts)) { + return i->second; + } + const auto result = count(); + cache->counts.emplace(key, result); + return result; +} + void AdvanceRevealLinesForBlock( const MarkdownArticlePaintContext &context, const LaidOutBlock &block, const style::Markdown &st) { if (context.reveal) { - context.reveal->nextLine += CountRevealLinesForBlock(block, st); + context.reveal->nextLine += CachedRevealLineCount( + context, + &block, + [&] { return CountRevealLinesForBlock(block, st); }); } } @@ -697,9 +718,16 @@ void PaintTextLeaf( const auto availableWidth = std::max(width, 1); auto linePostprocess = std::optional(); if (context.reveal && !elisionLines) { - const auto lines = leaf.countLinesGeometry(availableWidth, true); + const auto lineCount = CachedRevealLineCount( + context, + &leaf, + [&] { + return int(leaf.countLinesGeometry( + availableWidth, + true).size()); + }); const auto baseLine = context.reveal->nextLine; - context.reveal->nextLine += int(lines.size()); + context.reveal->nextLine += lineCount; if (const auto articlePostprocess = context.reveal->postprocess) { const auto activeLine = context.reveal->activeLine; linePostprocess.emplace(Ui::Text::LinePostprocess{ @@ -756,7 +784,10 @@ void PaintSelectableTextLeaf( && ((context.hiddenTextSegmentIndex == segmentIndex) || (context.hiddenSegmentIndex == segmentIndex))) { if (context.reveal && !elisionLines) { - context.reveal->nextLine += CountTextRevealLines(leaf, rect, width); + context.reveal->nextLine += CachedRevealLineCount( + context, + &leaf, + [&] { return CountTextRevealLines(leaf, rect, width); }); } return; } diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.cpp index 1b240344ab..c0e03cc7b6 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.cpp @@ -882,32 +882,41 @@ void RefreshScrollableSegmentRects( return; } for (const auto &block : blocks) { - const auto refreshIndex = [&](int index) { - if (index < 0 || index >= int(segments->size())) { - return; - } - RefreshBlockSegmentRect(block, &(*segments)[index]); - }; - refreshIndex(block.segmentIndex); - refreshIndex(block.secondarySegmentIndex); - refreshIndex(block.tertiarySegmentIndex); - if (block.kind == PreparedBlockKind::Table) { - for (const auto &row : block.tableRows) { - for (const auto &cell : row.cells) { - if (cell.segmentIndex < 0 - || cell.segmentIndex >= int(segments->size())) { - continue; - } - auto &segment = (*segments)[cell.segmentIndex]; - segment.outerRect = cell.outer; - segment.textRect = VisibleTextRect( - cell.textRect, - cell.outer); + RefreshScrollableSegmentRects(block, segments); + } +} + +void RefreshScrollableSegmentRects( + const LaidOutBlock &block, + std::vector *segments) { + if (!segments) { + return; + } + const auto refreshIndex = [&](int index) { + if (index < 0 || index >= int(segments->size())) { + return; + } + RefreshBlockSegmentRect(block, &(*segments)[index]); + }; + refreshIndex(block.segmentIndex); + refreshIndex(block.secondarySegmentIndex); + refreshIndex(block.tertiarySegmentIndex); + if (block.kind == PreparedBlockKind::Table) { + for (const auto &row : block.tableRows) { + for (const auto &cell : row.cells) { + if (cell.segmentIndex < 0 + || cell.segmentIndex >= int(segments->size())) { + continue; } + auto &segment = (*segments)[cell.segmentIndex]; + segment.outerRect = cell.outer; + segment.textRect = VisibleTextRect( + cell.textRect, + cell.outer); } } - RefreshScrollableSegmentRects(block.children, segments); } + RefreshScrollableSegmentRects(block.children, segments); } void CollectAnchors( diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.h index 7141e7a454..6d1f15e360 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.h @@ -60,6 +60,9 @@ void CollectSelectableSegments( void RefreshScrollableSegmentRects( const std::vector &blocks, std::vector *segments); +void RefreshScrollableSegmentRects( + const LaidOutBlock &block, + std::vector *segments); void CollectAnchors( const std::vector &blocks, std::vector> *anchors);