Animate slideshow slide switching with easing

Task: 2026/07/21/animate-slideshow-slide-transitions
This commit is contained in:
John Preston
2026-07-21 18:16:13 +04:00
parent f3371c76df
commit 9248dfbffe
@@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <unordered_set>
#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<HistoryView::Media*> 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<HistoryView::Media*> 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<HistoryView::Media*> 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<HistoryView::Media*> 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);
}