Optimize reveal animation of rich messages.

This commit is contained in:
John Preston
2026-06-11 14:27:37 +04:00
parent 65bb1a33b8
commit 67484a0c04
7 changed files with 110 additions and 32 deletions
@@ -2782,6 +2782,8 @@ private:
int _width = -1;
int _laidOutWidth = 0;
int _height = 0;
int _layoutGeneration = 0;
MarkdownArticleRevealLineCountsCache _revealLineCounts;
CachedTextLeafPool _cachedTextLeafs;
std::vector<LaidOutBlock> _blocks;
std::vector<LaidOutBlock> _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(
@@ -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<QImage*> 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<const void*, int> counts;
};
struct MarkdownArticleRevealPaintState {
int activeLine = -1;
int nextLine = 0;
const MarkdownArticleRevealPostprocess *postprocess = nullptr;
MarkdownArticleRevealLineCountsCache *lineCounts = nullptr;
};
struct MarkdownArticlePaintContext final : Ui::ChatPaintContext {
@@ -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));
@@ -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;
@@ -329,12 +329,33 @@ void PaintSelectableTextLeaf(
return result;
}
template <typename CountCallback>
[[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<Ui::Text::LinePostprocess>();
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;
}
@@ -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<SelectableSegment> *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(
@@ -60,6 +60,9 @@ void CollectSelectableSegments(
void RefreshScrollableSegmentRects(
const std::vector<LaidOutBlock> &blocks,
std::vector<SelectableSegment> *segments);
void RefreshScrollableSegmentRects(
const LaidOutBlock &block,
std::vector<SelectableSegment> *segments);
void CollectAnchors(
const std::vector<LaidOutBlock> &blocks,
std::vector<std::pair<QString, int>> *anchors);