From 9248dfbffe85b0242628833c699bb55b2e9d4295 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 21 Jul 2026 18:16:13 +0400 Subject: [PATCH] Animate slideshow slide switching with easing Task: 2026/07/21/animate-slideshow-slide-transitions --- .../iv_markdown_history_view_media.cpp | 121 +++++++++++++++--- 1 file changed, 102 insertions(+), 19 deletions(-) diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_history_view_media.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_history_view_media.cpp index ab3f889bf4..80573b830b 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_history_view_media.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_history_view_media.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include #include "settings.h" +#include "ui/effects/animations.h" #include "ui/image/image_location.h" #include "data/data_types.h" #include "data/data_auto_download.h" @@ -58,6 +59,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Iv::Markdown { namespace { +constexpr auto kSlideshowSlideDuration = crl::time(200); + class IvHistoryViewDelegate final : public HistoryView::SimpleElementDelegate { public: IvHistoryViewDelegate( @@ -765,10 +768,14 @@ private: void applyForcedSize(); + void applyForcedSizeFor(not_null media); + void ensureNavigationLinks(); void stepActiveIndex(int delta); + void startTransition(int from, int direction); + struct SlidePreload { std::shared_ptr<::Data::PhotoMedia> photo; std::shared_ptr<::Data::DocumentMedia> document; @@ -791,6 +798,13 @@ private: [[nodiscard]] IvHistoryViewHit resolveHit(QPoint point) const; + void paintSlide( + Painter &p, + const MarkdownArticlePaintContext &context, + QRect visible, + not_null media, + int shift) const; + [[nodiscard]] bool probeSupport(); void mediaPixelScaleUpdated() override; @@ -819,6 +833,9 @@ private: ClickHandlerPtr _previousLink; ClickHandlerPtr _nextLink; int _activeIndex = 0; + Ui::Animations::Simple _slideAnimation; + int _transitionFrom = -1; + int _transitionDirection = 0; int _requestedWidth = 0; bool _supported = false; MediaBlockHost *_registeredBridgeHost = nullptr; @@ -912,14 +929,8 @@ int IvHistoryViewSlideshowBlock::resizeGetHeight(int width) { return frameHeight(_requestedWidth); } -void IvHistoryViewSlideshowBlock::applyForcedSize() { - if (_geometry.isEmpty() || !alive()) { - return; - } - const auto media = activeMedia(); - if (!media) { - return; - } +void IvHistoryViewSlideshowBlock::applyForcedSizeFor( + not_null media) { const auto runtime = _host->view()->Get< HistoryView::InstantViewMediaRuntime>(); const auto guard = gsl::finally([&] { @@ -935,6 +946,22 @@ void IvHistoryViewSlideshowBlock::applyForcedSize() { media->resizeGetHeight(_geometry.width()); } +void IvHistoryViewSlideshowBlock::applyForcedSize() { + if (_geometry.isEmpty() || !alive()) { + return; + } + if (const auto media = activeMedia()) { + applyForcedSizeFor(media); + } + const auto count = int(_slides.size()); + if (_transitionFrom < 0 || _transitionFrom >= count) { + return; + } + if (const auto media = _slides[_transitionFrom].get()) { + applyForcedSizeFor(media); + } +} + void IvHistoryViewSlideshowBlock::setGeometry(QRect geometry) { if (!alive()) { _geometry = geometry; @@ -987,6 +1014,9 @@ void IvHistoryViewSlideshowBlock::unloadHeavyPart() { return; } const auto had = hasHeavyPart(); + _slideAnimation.stop(); + _transitionFrom = -1; + _transitionDirection = 0; _preloads.clear(); _residencyIndex = -1; _painted = false; @@ -1039,8 +1069,9 @@ void IvHistoryViewSlideshowBlock::setActiveItemIndex(int index) { if (next == _activeIndex) { return; } + const auto from = _activeIndex; _activeIndex = next; - applyForcedSize(); + startTransition(from, (next > from) ? 1 : -1); updatePreloads(); requestRepaint(_geometry); } @@ -1073,12 +1104,34 @@ void IvHistoryViewSlideshowBlock::stepActiveIndex(int delta) { if (next == _activeIndex) { return; } + const auto from = _activeIndex; _activeIndex = next; - applyForcedSize(); + startTransition(from, (delta > 0) ? 1 : -1); updatePreloads(); requestRepaint(_geometry); } +void IvHistoryViewSlideshowBlock::startTransition(int from, int direction) { + if (_geometry.isEmpty() || !_painted) { + applyForcedSize(); + return; + } + _transitionFrom = from; + _transitionDirection = direction; + applyForcedSize(); + _slideAnimation.stop(); + _slideAnimation.start([=] { + requestRepaint(_geometry); + }, 0., 1., kSlideshowSlideDuration, anim::easeOutCirc); + _slideAnimation.setFinishedCallback([=] { + _transitionFrom = -1; + _transitionDirection = 0; + _residencyIndex = -1; + requestRepaint(_geometry); + updatePreloads(); + }); +} + int IvHistoryViewSlideshowBlock::slideIndexAfter( int index, int delta) const { @@ -1226,7 +1279,7 @@ void IvHistoryViewSlideshowBlock::updatePreloads() { if (!_painted || !alive() || count < 2) { return; } - if (_residencyIndex != _activeIndex) { + if (_residencyIndex != _activeIndex && _transitionFrom < 0) { refreshResidency(); } if (!activeSlideReady()) { @@ -1337,6 +1390,21 @@ bool IvHistoryViewSlideshowBlock::probeSupport() { return true; } +void IvHistoryViewSlideshowBlock::paintSlide( + Painter &p, + const MarkdownArticlePaintContext &context, + QRect visible, + not_null media, + int shift) const { + p.save(); + const auto origin = _geometry.topLeft() + QPoint(shift, 0); + p.translate(origin); + auto local = context.translated(-origin); + local.clip = visible.translated(-origin); + media->draw(p, local); + p.restore(); +} + void IvHistoryViewSlideshowBlock::paint( Painter &p, const MarkdownArticlePaintContext &context) const { @@ -1355,12 +1423,26 @@ void IvHistoryViewSlideshowBlock::paint( RoundedRectPath(_geometry, st.radius), Qt::IntersectClip); - p.save(); - p.translate(_geometry.topLeft()); - auto local = context.translated(-_geometry.topLeft()); - local.clip = visible.translated(-_geometry.topLeft()); - media->draw(p, local); - p.restore(); + const auto count = int(_slides.size()); + const auto transition = _slideAnimation.animating() + && (_transitionFrom >= 0) + && (_transitionFrom < count) + && (_transitionFrom != _activeIndex); + if (transition) { + const auto progress = _slideAnimation.value(1.); + const auto width = _geometry.width(); + const auto direction = _transitionDirection; + const auto shift = anim::interpolate( + 0, + -direction * width, + progress); + if (const auto outgoing = _slides[_transitionFrom].get()) { + paintSlide(p, context, visible, outgoing, shift); + } + paintSlide(p, context, visible, media, shift + direction * width); + } else { + paintSlide(p, context, visible, media, 0); + } if (_slides.size() >= 2) { if (!_previousRect.isEmpty()) { @@ -1381,14 +1463,15 @@ void IvHistoryViewSlideshowBlock::paint( active ? st.navButtonBgOver : st.navButtonBg, active ? st.navNextIconOver : st.navNextIcon); } + const auto dotsIndex = transition ? _transitionFrom : _activeIndex; PaintSlideshowDots( p, ComputeSlideshowDots( _geometry, int(_slides.size()), - _activeIndex, + dotsIndex, st), - _activeIndex, + dotsIndex, st, _dotsBackdrop); }