/* 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 "ui/effects/thanos_effect_controller.h" #include "ui/effects/thanos_effect.h" #include "ui/chat/chat_style.h" #include "ui/painter.h" #include "data/data_session.h" #include "main/main_session.h" #include "history/history_item.h" #include "history/view/history_view_element.h" namespace Ui { namespace { constexpr auto kBaseDuration = crl::time(400); constexpr auto kPerPixelDuration = float64(0.15); constexpr auto kMaxDuration = crl::time(600); } // namespace ThanosEffectController::ThanosEffectController( not_null session, Delegate delegate, rpl::lifetime &lifetime) : _session(session) , _delegate(std::move(delegate)) { _session->data().itemsAboutToBeDestroyed( ) | rpl::on_next([=](const auto &items) { captureItemsBatch(items); }, lifetime); } ThanosEffectController::~ThanosEffectController() { _collapseAnimation.stop(); } void ThanosEffectController::captureItemsBatch( const std::vector> &items) { if (!ThanosEffect::Supported() || !ThanosEffect::WindowVisible(_delegate.window())) { return; } auto anyFound = false; for (const auto &item : items) { if (_delegate.viewForItem(item)) { anyFound = true; break; } } if (!anyFound) { return; } ensureScrollBaseline(); for (const auto &item : items) { if (const auto view = _delegate.viewForItem(item)) { const auto top = _delegate.itemTop(view); const auto height = view->height(); if (captureView(view, height, top)) { _preCaptured.emplace(item->fullId(), PreCapturedView{ .height = height, .top = top, .dateHeight = view->displayedDateHeight(), }); } } } } void ThanosEffectController::clearPreCaptured() { _preCaptured.clear(); if (!_collapseAnimation.animating()) { _restoreScrollPending = false; _wasAtBottom = false; } } void ThanosEffectController::captureOnRemoval( not_null item) { if (!ThanosEffect::Supported()) { return; } else if (!ThanosEffect::WindowVisible(_delegate.window())) { clearPreCaptured(); return; } const auto view = _delegate.viewForItem(item); if (!view) { return; } _removalHeight += view->height(); if (const auto it = _preCaptured.find(item->fullId()); it != end(_preCaptured)) { const auto saved = it->second; _preCaptured.erase(it); if (saved.top >= 0) { startCollapseAnimation( saved.height, saved.top, saved.dateHeight); } return; } const auto top = _delegate.itemTop(view); const auto height = view->height(); const auto dateHeight = view->displayedDateHeight(); if ((!item->isRegular() && !item->isEphemeral()) || item->isService() || top < 0 || height <= 0 || _delegate.contentWidth() <= 0) { return; } [[maybe_unused]] const auto dissolved = captureView(view, height, top); ensureScrollBaseline(); startCollapseAnimation(height, top, dateHeight); } void ThanosEffectController::ensureScrollBaseline() { if (!_restoreScrollPending) { _savedScrollTop = _delegate.scrollTop(); _wasAtBottom = (_savedScrollTop >= _delegate.scrollTopMax()); _restoreScrollPending = true; } } bool ThanosEffectController::captureView( not_null view, int viewHeight, int viewTop) { const auto item = view->data(); if ((!item->isRegular() && !item->isEphemeral()) || item->isService()) { return false; } if (viewTop < 0) { return false; } const auto viewWidth = _delegate.contentWidth(); if (viewWidth <= 0 || viewHeight <= 0) { return false; } const auto visibleTop = _delegate.visibleAreaTop(); const auto visibleBottom = _delegate.visibleAreaBottom(); const auto visibleHeight = visibleBottom - visibleTop; const auto screenTop = viewTop - visibleTop; if (screenTop + viewHeight <= 0 || screenTop >= visibleHeight) { return false; } auto gapOffset = 0; for (const auto &gap : _renderGaps) { if (gap.absY >= 0 && viewTop >= 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 false; } 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 = _delegate.preparePaintContext(clip); const auto renderedTop = viewTop + gapOffset; context.translate(0, -renderedTop); context.clip = clip; context.skipSelectionCheck = true; context.outbg = view->hasOutLayout(); view->draw(p, context); } const auto topLevel = _delegate.window(); if (!topLevel) { return false; } if (!_thanosEffect) { _thanosEffect = std::make_unique(topLevel); } _thanosEffect->setGeometry(QRect(QPoint(), topLevel->size())); _thanosEffect->raise(); const auto scroll = _delegate.scrollWidget(); const auto globalPos = scroll->mapTo( topLevel, QPoint(0, adjustedScreenTop + captureTop)); _thanosEffect->addItem( std::move(image), QRect(globalPos, QSize(viewWidth, captureHeight))); return true; } int CollapseDateShift(const std::vector &gaps, int itemTop) { for (const auto &gap : gaps) { if (gap.dateHeight > 0 && gap.absY + gap.height == itemTop) { return gap.height; } } return 0; } void ThanosEffectController::startCollapseAnimation( int height, int itemTop, int dateHeight) { if (height <= 0) { return; } const auto scrollTop = _delegate.scrollTop(); const auto scrollBottom = scrollTop + _delegate.scrollWidget()->height(); if (itemTop >= scrollBottom) { return; } // Gap positions are kept in the coordinates of the moment the first // gap was captured, while itemTop is measured in the current layout // that may have been shifted since (prepend / top margin change). itemTop -= _gapsShift; if (_collapseAnimation.animating()) { for (auto &gap : _collapseGaps) { gap.startHeight = gap.currentHeight; } _collapseAnimation = {}; } 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; gap.dateHeight = dateHeight; 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, .dateHeight = dateHeight, }); } auto totalHeight = 0; for (const auto &gap : _collapseGaps) { totalHeight += gap.currentHeight; } const auto duration = crl::time(std::clamp( kBaseDuration + totalHeight * kPerPixelDuration, float64(kBaseDuration), float64(kMaxDuration))); _collapseAnimation.start( [=] { collapseAnimationCallback(); }, 0., 1., duration, anim::halfSine); syncCollapseGapsToHost(); } void ThanosEffectController::collapseAnimationCallback() { const auto progress = _collapseAnimation.value(1.); auto sumOriginal = 0; auto sumCurrent = 0; for (auto &gap : _collapseGaps) { gap.currentHeight = anim::interpolate(gap.startHeight, 0, progress); sumOriginal += gap.originalHeight; sumCurrent += gap.currentHeight; } if (!_collapseGaps.empty()) { syncCollapseGapsToHost(); if (_wasAtBottom) { _delegate.scrollToY(_delegate.scrollTopMax()); } else { const auto collapsed = sumOriginal - sumCurrent; const auto target = std::max(_savedScrollTop - collapsed, 0); _delegate.scrollToY(target); } } if (!_collapseAnimation.animating()) { _collapseGaps.clear(); _renderGaps.clear(); _gapsShift = 0; _prependPending = false; _delegate.collapseGapsUpdated(); _collapseAnimation = {}; _restoreScrollPending = false; _wasAtBottom = false; } } void ThanosEffectController::pinScroll() { if (_collapseGaps.empty() || _inPinScroll) { return; } _inPinScroll = true; const auto guard = gsl::finally([&] { _inPinScroll = false; }); if (_wasAtBottom) { _delegate.scrollToY(_delegate.scrollTopMax()); return; } auto collapsed = 0; for (const auto &gap : _collapseGaps) { collapsed += gap.originalHeight - gap.currentHeight; } _delegate.scrollToY(std::max(_savedScrollTop - collapsed, 0)); } void ThanosEffectController::shiftGaps(int delta) { if (!delta || _collapseGaps.empty()) { return; } _gapsShift += delta; for (auto &gap : _renderGaps) { gap.absY += delta; } if (_restoreScrollPending && !_wasAtBottom) { // The content above the gaps moved, so the scroll position that // keeps the same content visible moved by the same delta. Without // this the next animation frame forces the pre-shift coordinate. _savedScrollTop += delta; } } void ThanosEffectController::notePrependBaseline(int contentHeight) { if (_collapseGaps.empty() || _prependPending) { return; } _prependBaseline = contentHeight; _prependPending = true; } void ThanosEffectController::applyPrependBaseline(int contentHeight) { if (!_prependPending) { return; } _prependPending = false; const auto delta = contentHeight - _prependBaseline; if (delta > 0) { shiftGaps(delta); } } void ThanosEffectController::syncCollapseGapsToHost() { auto gaps = std::vector(); gaps.reserve(_collapseGaps.size()); auto cumulativeOriginal = 0; for (const auto &g : _collapseGaps) { gaps.push_back({ .absY = g.absY - cumulativeOriginal + _gapsShift, .height = g.currentHeight, .dateHeight = g.dateHeight, }); cumulativeOriginal += g.originalHeight; } if (_renderGaps != gaps) { _renderGaps = std::move(gaps); _delegate.collapseGapsUpdated(); } } } // namespace Ui