From 1167523f4a7febf175e06f521b79693f481d3c0a Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 9 Apr 2026 22:10:54 +0300 Subject: [PATCH] [thanos] Pre-captured snapshots before batch destruction via event. --- Telegram/SourceFiles/data/data_histories.cpp | 3 + Telegram/SourceFiles/data/data_session.cpp | 10 +++ Telegram/SourceFiles/data/data_session.h | 5 ++ .../history/history_inner_widget.cpp | 76 +++++++++++-------- .../history/history_inner_widget.h | 7 ++ 5 files changed, 69 insertions(+), 32 deletions(-) diff --git a/Telegram/SourceFiles/data/data_histories.cpp b/Telegram/SourceFiles/data/data_histories.cpp index 534ae0dd24..7968fbc0a1 100644 --- a/Telegram/SourceFiles/data/data_histories.cpp +++ b/Telegram/SourceFiles/data/data_histories.cpp @@ -988,6 +988,9 @@ void Histories::deleteMessages(const MessageIdsList &ids, bool revoke) { document->owner().savedMusic().remove(document); } + if (!remove.empty()) { + _owner->notifyItemsAboutToBeDestroyed(remove); + } for (const auto &item : remove) { const auto history = item->history(); const auto wasLast = (history->lastMessage() == item); diff --git a/Telegram/SourceFiles/data/data_session.cpp b/Telegram/SourceFiles/data/data_session.cpp index 2d74bb9389..7d2e394758 100644 --- a/Telegram/SourceFiles/data/data_session.cpp +++ b/Telegram/SourceFiles/data/data_session.cpp @@ -2182,6 +2182,16 @@ rpl::producer> Session::itemRemoved( }); } +void Session::notifyItemsAboutToBeDestroyed( + const std::vector> &items) { + _itemsAboutToBeDestroyed.fire_copy(items); +} + +auto Session::itemsAboutToBeDestroyed() const +-> rpl::producer>> { + return _itemsAboutToBeDestroyed.events(); +} + void Session::notifyViewAboutToBeRemoved( not_null view) { _viewAboutToBeRemoved.fire_copy(view); diff --git a/Telegram/SourceFiles/data/data_session.h b/Telegram/SourceFiles/data/data_session.h index 265eff943f..0134ed09f6 100644 --- a/Telegram/SourceFiles/data/data_session.h +++ b/Telegram/SourceFiles/data/data_session.h @@ -427,6 +427,10 @@ public: [[nodiscard]] rpl::producer> itemRemoved() const; [[nodiscard]] rpl::producer> itemRemoved( FullMsgId itemId) const; + void notifyItemsAboutToBeDestroyed( + const std::vector> &items); + [[nodiscard]] auto itemsAboutToBeDestroyed() const + -> rpl::producer>>; void notifyViewAboutToBeRemoved(not_null view); [[nodiscard]] rpl::producer> viewAboutToBeRemoved() const; void notifyViewRemoved(not_null view); @@ -1173,6 +1177,7 @@ private: rpl::event_stream> _itemDataChanges; rpl::event_stream _reactionsRemoved; rpl::event_stream> _itemRemoved; + rpl::event_stream>> _itemsAboutToBeDestroyed; rpl::event_stream> _viewAboutToBeRemoved; rpl::event_stream> _viewRemoved; rpl::event_stream> _viewPaidReactionSent; diff --git a/Telegram/SourceFiles/history/history_inner_widget.cpp b/Telegram/SourceFiles/history/history_inner_widget.cpp index 29edd4b815..5b08169948 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.cpp +++ b/Telegram/SourceFiles/history/history_inner_widget.cpp @@ -435,6 +435,10 @@ HistoryInner::HistoryInner( ) | rpl::on_next( [this](auto item) { itemRemoved(item); }, lifetime()); + session().data().itemsAboutToBeDestroyed( + ) | rpl::on_next( + [this](const auto &items) { captureItemsForThanosEffect(items); }, + lifetime()); session().data().viewAboutToBeRemoved( ) | rpl::on_next( [this](auto view) { captureViewForThanosEffect(view); }, @@ -4196,11 +4200,43 @@ void HistoryInner::leaveEventHook(QEvent *e) { return RpWidget::leaveEventHook(e); } +void HistoryInner::captureItemsForThanosEffect( + const std::vector> &items) { + if (!Ui::ThanosEffect::Supported()) { + return; + } + // Pre-capture all views while they are in their original visual + // state, before any item->destroy() changes neighbor views' + // attach/date flags via previousInBlocksChanged(). + for (const auto &item : items) { + if (const auto view = item->mainView()) { + const auto top = itemTop(view); + const auto height = view->height(); + captureViewForThanosEffect(view); + _thanosPreCaptured.emplace(view, PreCapturedView{ + .height = height, + .top = top, + }); + } + } +} + void HistoryInner::captureViewForThanosEffect( not_null view) { if (!Ui::ThanosEffect::Supported()) { return; } + // Skip views that were already pre-captured in the batch signal. + // Use the saved height/top from before any destruction started. + if (const auto it = _thanosPreCaptured.find(view); + it != end(_thanosPreCaptured)) { + const auto saved = it->second; + _thanosPreCaptured.erase(it); + if (saved.top >= 0) { + _widget->startCollapseAnimation(saved.height, saved.top); + } + return; + } const auto item = view->data(); if (item->history() != _history && item->history() != _migrated) { @@ -4213,42 +4249,25 @@ void HistoryInner::captureViewForThanosEffect( if (top < 0) { return; } - // The visual height as it was displayed on screen (for positioning). - const auto displayHeight = view->height(); + const auto viewHeight = view->height(); const auto viewWidth = width(); - if (viewWidth <= 0 || displayHeight <= 0) { + if (viewWidth <= 0 || viewHeight <= 0) { return; } const auto visibleHeight = _visibleAreaBottom - _visibleAreaTop; const auto screenTop = top - _visibleAreaTop; - if (screenTop + displayHeight <= 0 || screenTop >= visibleHeight) { + if (screenTop + viewHeight <= 0 || screenTop >= visibleHeight) { return; } - - // If a prior removal in the same batch changed this view's - // attach/date flags via previousInBlocksChanged(), the view has - // pendingResize: draw() uses the new flags but height() is stale. - // Force a re-layout so height() matches what draw() will produce. - // The extra height goes to the top (larger marginTop), so we offset - // drawing to capture only the content region matching the on-screen view. - const auto hadPendingResize = view->pendingResize(); - if (hadPendingResize) { - const_cast(view.get())->resizeGetHeight(viewWidth); - } - const auto viewHeight = view->height(); - // Extra height added at top after re-layout (0 if no resize happened). - const auto heightDelta = viewHeight - displayHeight; - const auto gapOffset = (_collapseGapAbsY >= 0 && top >= _collapseGapAbsY) ? _collapseGapHeight : 0; const auto adjustedScreenTop = screenTop + gapOffset; - // Clip based on displayHeight — that's what was visible on screen. - const auto captureTop = std::clamp(-adjustedScreenTop, 0, displayHeight); + const auto captureTop = std::clamp(-adjustedScreenTop, 0, viewHeight); const auto captureBottom = std::clamp( visibleHeight - adjustedScreenTop, 0, - displayHeight); + viewHeight); if (captureTop >= captureBottom) { return; } @@ -4262,11 +4281,8 @@ void HistoryInner::captureViewForThanosEffect( image.fill(Qt::transparent); { Painter p(&image); - // Offset by heightDelta to skip the extra top margin added by - // re-layout, so the captured content matches what was on screen. - const auto drawTop = captureTop + heightDelta; - p.translate(0, -drawTop); - auto clip = QRect(0, drawTop, viewWidth, captureHeight); + p.translate(0, -captureTop); + auto clip = QRect(0, captureTop, viewWidth, captureHeight); auto context = preparePaintContext(clip); const auto renderedTop = top + gapOffset; context.translate(0, -renderedTop); @@ -4287,8 +4303,6 @@ void HistoryInner::captureViewForThanosEffect( _thanosEffect->setGeometry(QRect(QPoint(), topLevel->size())); _thanosEffect->raise(); - // Place at the original screen position (no heightDelta shift needed, - // since the image already contains only the on-screen content region). const auto globalPos = _scroll->mapTo( topLevel, QPoint(0, adjustedScreenTop + captureTop)); @@ -4296,9 +4310,7 @@ void HistoryInner::captureViewForThanosEffect( std::move(image), QRect(globalPos, QSize(viewWidth, captureHeight))); - // Use the original display height for collapse gap calculation, - // as that's how much visual space the view occupied on screen. - _widget->startCollapseAnimation(displayHeight, top); + _widget->startCollapseAnimation(viewHeight, top); } HistoryInner::~HistoryInner() { diff --git a/Telegram/SourceFiles/history/history_inner_widget.h b/Telegram/SourceFiles/history/history_inner_widget.h index 7f4c78ae29..cf32dd9587 100644 --- a/Telegram/SourceFiles/history/history_inner_widget.h +++ b/Telegram/SourceFiles/history/history_inner_widget.h @@ -610,6 +610,8 @@ private: [[nodiscard]] HistoryView::ElementOverlayHost &ensureOverlayHost(); std::unique_ptr _overlayHost; + void captureItemsForThanosEffect( + const std::vector> &items); void captureViewForThanosEffect(not_null view); public: @@ -617,6 +619,11 @@ public: private: std::unique_ptr _thanosEffect; + struct PreCapturedView { + int height = 0; + int top = 0; + }; + base::flat_map, PreCapturedView> _thanosPreCaptured; int _collapseGapAbsY = -1; int _collapseGapHeight = 0;