[thanos] Migrated history inner widget to ThanosEffectController.

This commit is contained in:
23rd
2026-04-10 10:31:10 +03:00
parent 3e416e0bf6
commit 8d0e1ab2ee
4 changed files with 44 additions and 287 deletions
@@ -435,14 +435,7 @@ 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); },
lifetime());
setupThanosEffect();
session().data().viewRemoved(
) | rpl::on_next(
[this](auto view) { viewRemoved(view); },
@@ -4105,9 +4098,9 @@ void HistoryInner::setItemsRevealHeight(int revealHeight) {
_revealHeight = revealHeight;
}
void HistoryInner::setCollapseGaps(const std::vector<CollapseGap> &gaps) {
void HistoryInner::setCollapseGaps(std::vector<CollapseGap> gaps) {
if (_collapseGaps != gaps) {
_collapseGaps = gaps;
_collapseGaps = std::move(gaps);
updateSize();
}
}
@@ -4203,126 +4196,43 @@ void HistoryInner::leaveEventHook(QEvent *e) {
return RpWidget::leaveEventHook(e);
}
void HistoryInner::captureItemsForThanosEffect(
const std::vector<not_null<HistoryItem*>> &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().
_suppressCollapseAnimation = true;
const auto guard = gsl::finally([&] {
_suppressCollapseAnimation = false;
});
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::setupThanosEffect() {
const auto history = _history;
const auto migrated = [=] { return _migrated; };
_thanosController = std::make_unique<Ui::ThanosEffectController>(
&session(),
Ui::ThanosEffectController::Delegate{
.viewForItem = [=](not_null<const HistoryItem*> item)
-> HistoryView::Element* {
if (item->history() != history
&& item->history() != migrated()) {
return nullptr;
}
return item->mainView();
},
.itemTop = [=](not_null<const HistoryView::Element*> view) {
return itemTop(view);
},
.visibleAreaTop = [=] { return _visibleAreaTop; },
.visibleAreaBottom = [=] { return _visibleAreaBottom; },
.contentWidth = [=] { return width(); },
.preparePaintContext = [=](QRect clip) {
return preparePaintContext(clip);
},
.window = [=]() -> QWidget* { return window(); },
.scrollArea = [=]() -> not_null<Ui::ScrollArea*> {
return _scroll;
},
.setCollapseGaps = [=](std::vector<CollapseGap> gaps) {
setCollapseGaps(std::move(gaps));
},
},
lifetime());
void HistoryInner::captureViewForThanosEffect(
not_null<const Element*> 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) {
return;
}
if (!item->isRegular() || item->isService()) {
return;
}
const auto top = itemTop(view);
if (top < 0) {
return;
}
const auto viewHeight = view->height();
const auto viewWidth = width();
if (viewWidth <= 0 || viewHeight <= 0) {
return;
}
const auto visibleHeight = _visibleAreaBottom - _visibleAreaTop;
const auto screenTop = top - _visibleAreaTop;
if (screenTop + viewHeight <= 0 || screenTop >= visibleHeight) {
return;
}
auto gapOffset = 0;
for (const auto &gap : _collapseGaps) {
if (gap.absY >= 0 && top >= gap.absY) {
gapOffset += gap.height;
}
}
const auto adjustedScreenTop = screenTop + gapOffset;
const auto captureTop = std::clamp(-adjustedScreenTop, 0, viewHeight);
const auto captureBottom = std::clamp(
visibleHeight - adjustedScreenTop,
0,
viewHeight);
if (captureTop >= captureBottom) {
return;
}
const auto captureHeight = captureBottom - captureTop;
const auto dpr = style::DevicePixelRatio();
auto image = QImage(
QSize(viewWidth, captureHeight) * dpr,
QImage::Format_RGBA8888_Premultiplied);
image.setDevicePixelRatio(dpr);
image.fill(Qt::transparent);
{
Painter p(&image);
p.translate(0, -captureTop);
auto clip = QRect(0, captureTop, viewWidth, captureHeight);
auto context = preparePaintContext(clip);
const auto renderedTop = top + gapOffset;
context.translate(0, -renderedTop);
context.clip = clip;
context.skipSelectionCheck = true;
context.outbg = view->hasOutLayout();
view->draw(p, context);
}
const auto topLevel = window();
if (!topLevel) {
return;
}
if (!_thanosEffect) {
_thanosEffect = std::make_unique<Ui::ThanosEffect>(topLevel);
}
_thanosEffect->setGeometry(QRect(QPoint(), topLevel->size()));
_thanosEffect->raise();
const auto globalPos = _scroll->mapTo(
topLevel,
QPoint(0, adjustedScreenTop + captureTop));
_thanosEffect->addItem(
std::move(image),
QRect(globalPos, QSize(viewWidth, captureHeight)));
if (!_suppressCollapseAnimation) {
_widget->startCollapseAnimation(viewHeight, top);
}
session().data().viewAboutToBeRemoved(
) | rpl::on_next([=](not_null<const HistoryView::Element*> view) {
_thanosController->captureOnRemoval(view->data());
}, lifetime());
}
HistoryInner::~HistoryInner() {
@@ -12,7 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/rp_widget.h"
#include "ui/controls/swipe_handler_data.h"
#include "ui/effects/animations.h"
#include "ui/effects/thanos_effect.h"
#include "ui/effects/thanos_effect_controller.h"
#include "ui/dragging_scroll_manager.h"
#include "ui/widgets/middle_click_autoscroll.h"
#include "ui/widgets/tooltip.h"
@@ -118,13 +118,8 @@ public:
Ui::ChatPaintContext preparePaintContext(const QRect &clip) const;
struct CollapseGap {
int absY = -1;
int height = 0;
friend bool operator==(const CollapseGap&, const CollapseGap&) = default;
};
void setCollapseGaps(const std::vector<CollapseGap> &gaps);
using CollapseGap = Ui::CollapseGap;
void setCollapseGaps(std::vector<CollapseGap> gaps);
void messagesReceived(
not_null<PeerData*> peer,
@@ -618,17 +613,9 @@ private:
[[nodiscard]] HistoryView::ElementOverlayHost &ensureOverlayHost();
std::unique_ptr<HistoryView::ElementOverlayHost> _overlayHost;
void captureItemsForThanosEffect(
const std::vector<not_null<HistoryItem*>> &items);
void captureViewForThanosEffect(not_null<const Element*> view);
void setupThanosEffect();
std::unique_ptr<Ui::ThanosEffect> _thanosEffect;
struct PreCapturedView {
int height = 0;
int top = 0;
};
base::flat_map<not_null<const Element*>, PreCapturedView> _thanosPreCaptured;
bool _suppressCollapseAnimation = false;
std::unique_ptr<Ui::ThanosEffectController> _thanosController;
std::vector<CollapseGap> _collapseGaps;
};
@@ -7558,133 +7558,6 @@ void HistoryWidget::startItemRevealAnimations() {
}
}
void HistoryWidget::startCollapseAnimation(int height, int itemTop) {
constexpr auto kBaseMs = 400;
constexpr auto kPerPixelMs = 0.5;
constexpr auto kMaxMs = 1200;
if (height <= 0 || !_list) {
return;
}
const auto scrollTop = _scroll->scrollTop();
const auto scrollBottom = scrollTop + _scroll->height();
const auto itemBottom = itemTop + height;
if (itemBottom >= scrollBottom) {
return;
}
if (_collapseAnimation.animating()) {
for (auto &gap : _collapseGaps) {
gap.startHeight = gap.currentHeight;
}
}
auto merged = false;
for (auto &gap : _collapseGaps) {
if (gap.absY + gap.originalHeight == itemTop
|| (gap.absY <= itemTop
&& itemTop <= gap.absY + gap.originalHeight)) {
gap.startHeight += height;
gap.currentHeight += height;
gap.originalHeight += height;
merged = true;
break;
}
if (itemTop + height == gap.absY) {
gap.absY = itemTop;
gap.startHeight += height;
gap.currentHeight += height;
gap.originalHeight += height;
merged = true;
break;
}
}
if (!merged) {
const auto it = std::lower_bound(
_collapseGaps.begin(),
_collapseGaps.end(),
itemTop,
[](const auto &gap, int top) { return gap.absY < top; });
_collapseGaps.insert(it, {
.absY = itemTop,
.startHeight = height,
.currentHeight = height,
.originalHeight = height,
});
}
syncCollapseGapsToList();
const auto aboveViewport = std::max(0, scrollTop - itemTop);
const auto scrollAdjust = std::min(height, aboveViewport);
if (scrollAdjust > 0) {
synteticScrollToY(scrollTop + scrollAdjust);
}
auto totalHeight = 0;
for (const auto &gap : _collapseGaps) {
totalHeight += gap.currentHeight;
}
const auto duration = int(std::clamp(
kBaseMs + totalHeight * kPerPixelMs,
double(kBaseMs),
double(kMaxMs)));
_collapseAnimation.start(
[=] { collapseAnimationCallback(); },
0.,
1.,
duration,
anim::easeOutCirc);
}
void HistoryWidget::collapseAnimationCallback() {
const auto progress = _collapseAnimation.value(1.);
auto totalDelta = 0;
for (auto &gap : _collapseGaps) {
const auto newHeight = anim::interpolate(
gap.startHeight,
0,
progress);
totalDelta += (gap.currentHeight - newHeight);
gap.currentHeight = newHeight;
}
if (totalDelta != 0) {
const auto scrollTop = _scroll->scrollTop();
syncCollapseGapsToList();
synteticScrollToY(std::max(scrollTop - totalDelta, 0));
}
if (!_collapseAnimation.animating()) {
_collapseGaps.clear();
if (_list) {
_list->setCollapseGaps({});
}
_collapseAnimation = {};
}
}
void HistoryWidget::syncCollapseGapsToList() {
if (!_list) {
return;
}
auto gaps = std::vector<HistoryInner::CollapseGap>();
gaps.reserve(_collapseGaps.size());
auto cumulativeOriginal = 0;
for (const auto &g : _collapseGaps) {
gaps.push_back({
.absY = g.absY - cumulativeOriginal,
.height = g.currentHeight,
});
cumulativeOriginal += g.originalHeight;
}
_list->setCollapseGaps(gaps);
}
void HistoryWidget::startMessageSendingAnimation(
not_null<HistoryItem*> item) {
if (_list->elementChatMode() == HistoryView::ElementChatMode::Default
@@ -338,9 +338,6 @@ protected:
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
public:
void startCollapseAnimation(int height, int itemTop);
private:
using TabbedPanel = ChatHelpers::TabbedPanel;
using TabbedSelector = ChatHelpers::TabbedSelector;
@@ -945,16 +942,6 @@ private:
ItemRevealAnimation> _itemRevealAnimations;
int _itemsRevealHeight = 0;
struct CollapseGapState {
int absY = -1;
int startHeight = 0;
int currentHeight = 0;
int originalHeight = 0;
};
std::vector<CollapseGapState> _collapseGaps;
Ui::Animations::Simple _collapseAnimation;
void collapseAnimationCallback();
void syncCollapseGapsToList();
bool _sponsoredMessagesStateKnown = false;
bool _justMarkingAsRead = false;