401 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			401 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| 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/animated_icon.h"
 | |
| 
 | |
| #include "ui/image/image_prepare.h"
 | |
| #include "ui/style/style_core.h"
 | |
| #include "ui/effects/frame_generator.h"
 | |
| 
 | |
| #include <QtGui/QPainter>
 | |
| #include <crl/crl_async.h>
 | |
| #include <crl/crl_semaphore.h>
 | |
| #include <crl/crl_on_main.h>
 | |
| 
 | |
| namespace Ui {
 | |
| namespace {
 | |
| 
 | |
| constexpr auto kDefaultDuration = crl::time(800);
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| struct AnimatedIcon::Frame {
 | |
| 	FrameGenerator::Frame generated;
 | |
| 	QImage resizedImage;
 | |
| 	int index = 0;
 | |
| };
 | |
| 
 | |
| class AnimatedIcon::Impl final : public std::enable_shared_from_this<Impl> {
 | |
| public:
 | |
| 	explicit Impl(base::weak_ptr<AnimatedIcon> weak);
 | |
| 
 | |
| 	void prepareFromAsync(
 | |
| 		FnMut<std::unique_ptr<FrameGenerator>()> factory,
 | |
| 		QSize sizeOverride);
 | |
| 	void waitTillPrepared() const;
 | |
| 
 | |
| 	[[nodiscard]] bool valid() const;
 | |
| 	[[nodiscard]] QSize size() const;
 | |
| 	[[nodiscard]] int framesCount() const;
 | |
| 	[[nodiscard]] double frameRate() const;
 | |
| 	[[nodiscard]] Frame &frame();
 | |
| 	[[nodiscard]] const Frame &frame() const;
 | |
| 
 | |
| 	[[nodiscard]] crl::time animationDuration() const;
 | |
| 	void moveToFrame(int frame, QSize updatedDesiredSize);
 | |
| 
 | |
| private:
 | |
| 	enum class PreloadState {
 | |
| 		None,
 | |
| 		Preloading,
 | |
| 		Ready,
 | |
| 	};
 | |
| 
 | |
| 	// Called from crl::async.
 | |
| 	void renderPreloadFrame();
 | |
| 
 | |
| 	std::unique_ptr<FrameGenerator> _generator;
 | |
| 	Frame _current;
 | |
| 	QSize _desiredSize;
 | |
| 	std::atomic<PreloadState> _preloadState = PreloadState::None;
 | |
| 
 | |
| 	Frame _preloaded; // Changed on main or async depending on _preloadState.
 | |
| 	QSize _preloadImageSize;
 | |
| 
 | |
| 	base::weak_ptr<AnimatedIcon> _weak;
 | |
| 	int _framesCount = 0;
 | |
| 	double _frameRate = 0.;
 | |
| 	mutable crl::semaphore _semaphore;
 | |
| 	mutable bool _ready = false;
 | |
| 
 | |
| };
 | |
| 
 | |
| AnimatedIcon::Impl::Impl(base::weak_ptr<AnimatedIcon> weak)
 | |
| : _weak(weak) {
 | |
| }
 | |
| 
 | |
| void AnimatedIcon::Impl::prepareFromAsync(
 | |
| 		FnMut<std::unique_ptr<FrameGenerator>()> factory,
 | |
| 		QSize sizeOverride) {
 | |
| 	const auto guard = gsl::finally([&] { _semaphore.release(); });
 | |
| 	if (!_weak) {
 | |
| 		return;
 | |
| 	}
 | |
| 	auto generator = factory ? factory() : nullptr;
 | |
| 	if (!generator || !_weak) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_framesCount = generator->count();
 | |
| 	_frameRate = generator->rate();
 | |
| 	_current.generated = generator->renderNext(QImage(), sizeOverride);
 | |
| 	if (_current.generated.image.isNull()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	_generator = std::move(generator);
 | |
| 	_desiredSize = sizeOverride.isEmpty()
 | |
| 		? style::ConvertScale(_current.generated.image.size())
 | |
| 		: sizeOverride;
 | |
| }
 | |
| 
 | |
| void AnimatedIcon::Impl::waitTillPrepared() const {
 | |
| 	if (!_ready) {
 | |
| 		_semaphore.acquire();
 | |
| 		_ready = true;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| bool AnimatedIcon::Impl::valid() const {
 | |
| 	waitTillPrepared();
 | |
| 	return (_generator != nullptr);
 | |
| }
 | |
| 
 | |
| QSize AnimatedIcon::Impl::size() const {
 | |
| 	waitTillPrepared();
 | |
| 	return _desiredSize;
 | |
| }
 | |
| 
 | |
| int AnimatedIcon::Impl::framesCount() const {
 | |
| 	waitTillPrepared();
 | |
| 	return _framesCount;
 | |
| }
 | |
| 
 | |
| double AnimatedIcon::Impl::frameRate() const {
 | |
| 	waitTillPrepared();
 | |
| 	return _frameRate;
 | |
| }
 | |
| 
 | |
| AnimatedIcon::Frame &AnimatedIcon::Impl::frame() {
 | |
| 	waitTillPrepared();
 | |
| 	return _current;
 | |
| }
 | |
| 
 | |
| const AnimatedIcon::Frame &AnimatedIcon::Impl::frame() const {
 | |
| 	waitTillPrepared();
 | |
| 	return _current;
 | |
| }
 | |
| 
 | |
| crl::time AnimatedIcon::Impl::animationDuration() const {
 | |
| 	waitTillPrepared();
 | |
| 	const auto rate = _generator ? _generator->rate() : 0.;
 | |
| 	const auto frames = _generator ? _generator->count() : 0;
 | |
| 	return (frames && rate >= 1.)
 | |
| 		? crl::time(base::SafeRound(frames / rate * 1000.))
 | |
| 		: 0;
 | |
| }
 | |
| 
 | |
| void AnimatedIcon::Impl::moveToFrame(int frame, QSize updatedDesiredSize) {
 | |
| 	waitTillPrepared();
 | |
| 	const auto state = _preloadState.load();
 | |
| 	const auto shown = _current.index;
 | |
| 	if (!updatedDesiredSize.isEmpty()) {
 | |
| 		_desiredSize = updatedDesiredSize;
 | |
| 	}
 | |
| 	const auto desiredImageSize = _desiredSize * style::DevicePixelRatio();
 | |
| 	if (!_generator
 | |
| 		|| state == PreloadState::Preloading
 | |
| 		|| (shown == frame
 | |
| 			&& (_current.generated.image.size() == desiredImageSize))) {
 | |
| 		return;
 | |
| 	} else if (state == PreloadState::Ready) {
 | |
| 		if (_preloaded.index == frame
 | |
| 			&& (shown != frame
 | |
| 				|| _preloaded.generated.image.size() == desiredImageSize)) {
 | |
| 			std::swap(_current, _preloaded);
 | |
| 			if (_current.generated.image.size() == desiredImageSize) {
 | |
| 				return;
 | |
| 			}
 | |
| 		} else if ((shown < _preloaded.index && _preloaded.index < frame)
 | |
| 			|| (shown > _preloaded.index && _preloaded.index > frame)) {
 | |
| 			std::swap(_current, _preloaded);
 | |
| 		}
 | |
| 	}
 | |
| 	_preloadImageSize = desiredImageSize;
 | |
| 	_preloaded.index = frame;
 | |
| 	_preloadState = PreloadState::Preloading;
 | |
| 	crl::async([guard = shared_from_this()] {
 | |
| 		guard->renderPreloadFrame();
 | |
| 	});
 | |
| }
 | |
| 
 | |
| void AnimatedIcon::Impl::renderPreloadFrame() {
 | |
| 	if (!_weak) {
 | |
| 		return;
 | |
| 	}
 | |
| 	if (_preloaded.index == 0) {
 | |
| 		_generator->jumpToStart();
 | |
| 	}
 | |
| 	_preloaded.generated = (_preloaded.index && _preloaded.index == _current.index)
 | |
| 		? _generator->renderCurrent(
 | |
| 			std::move(_preloaded.generated.image),
 | |
| 			_preloadImageSize)
 | |
| 		: _generator->renderNext(
 | |
| 			std::move(_preloaded.generated.image),
 | |
| 			_preloadImageSize);
 | |
| 	_preloaded.resizedImage = QImage();
 | |
| 	_preloadState = PreloadState::Ready;
 | |
| 	crl::on_main(_weak, [=] {
 | |
| 		_weak->frameJumpFinished();
 | |
| 	});
 | |
| }
 | |
| 
 | |
| AnimatedIcon::AnimatedIcon(AnimatedIconDescriptor &&descriptor)
 | |
| : _impl(std::make_shared<Impl>(base::make_weak(this)))
 | |
| , _colorized(descriptor.colorized) {
 | |
| 	crl::async([
 | |
| 		impl = _impl,
 | |
| 		factory = std::move(descriptor.generator),
 | |
| 		sizeOverride = descriptor.sizeOverride
 | |
| 	]() mutable {
 | |
| 		impl->prepareFromAsync(std::move(factory), sizeOverride);
 | |
| 	});
 | |
| }
 | |
| 
 | |
| void AnimatedIcon::wait() const {
 | |
| 	_impl->waitTillPrepared();
 | |
| }
 | |
| 
 | |
| bool AnimatedIcon::valid() const {
 | |
| 	return _impl->valid();
 | |
| }
 | |
| 
 | |
| int AnimatedIcon::frameIndex() const {
 | |
| 	return _impl->frame().index;
 | |
| }
 | |
| 
 | |
| int AnimatedIcon::framesCount() const {
 | |
| 	return _impl->framesCount();
 | |
| }
 | |
| 
 | |
| double AnimatedIcon::frameRate() const {
 | |
| 	return _impl->frameRate();
 | |
| }
 | |
| 
 | |
| QImage AnimatedIcon::frame(const QColor &textColor) const {
 | |
| 	return frame(textColor, QSize(), nullptr).image;
 | |
| }
 | |
| 
 | |
| QImage AnimatedIcon::notColorizedFrame() const {
 | |
| 	return notColorizedFrame(QSize(), nullptr).image;
 | |
| }
 | |
| 
 | |
| AnimatedIcon::ResizedFrame AnimatedIcon::frame(
 | |
| 		const QColor &textColor,
 | |
| 		QSize desiredSize,
 | |
| 		Fn<void()> updateWithPerfect) const {
 | |
| 	auto result = notColorizedFrame(
 | |
| 		desiredSize,
 | |
| 		std::move(updateWithPerfect));
 | |
| 	if (_colorized) {
 | |
| 		auto &image = result.image;
 | |
| 		style::colorizeImage(image, textColor, &image, {}, {}, true);
 | |
| 	}
 | |
| 	return result;
 | |
| }
 | |
| 
 | |
| AnimatedIcon::ResizedFrame AnimatedIcon::notColorizedFrame(
 | |
| 		QSize desiredSize,
 | |
| 		Fn<void()> updateWithPerfect) const {
 | |
| 	auto &frame = _impl->frame();
 | |
| 	preloadNextFrame(crl::now(), &frame, desiredSize);
 | |
| 	const auto desired = size() * style::DevicePixelRatio();
 | |
| 	if (frame.generated.image.isNull()) {
 | |
| 		return { frame.generated.image };
 | |
| 	} else if (frame.generated.image.size() == desired) {
 | |
| 		return { frame.generated.image };
 | |
| 	} else if (frame.resizedImage.size() != desired) {
 | |
| 		frame.resizedImage = frame.generated.image.scaled(
 | |
| 			desired,
 | |
| 			Qt::IgnoreAspectRatio,
 | |
| 			Qt::SmoothTransformation);
 | |
| 	}
 | |
| 	if (updateWithPerfect) {
 | |
| 		_repaint = std::move(updateWithPerfect);
 | |
| 	}
 | |
| 	return { frame.resizedImage, true };
 | |
| }
 | |
| 
 | |
| int AnimatedIcon::width() const {
 | |
| 	return size().width();
 | |
| }
 | |
| 
 | |
| int AnimatedIcon::height() const {
 | |
| 	return size().height();
 | |
| }
 | |
| 
 | |
| QSize AnimatedIcon::size() const {
 | |
| 	return _impl->size();
 | |
| }
 | |
| 
 | |
| void AnimatedIcon::paint(QPainter &p, int x, int y) {
 | |
| 	auto &frame = _impl->frame();
 | |
| 	preloadNextFrame(crl::now(), &frame);
 | |
| 	if (frame.generated.image.isNull()) {
 | |
| 		return;
 | |
| 	}
 | |
| 	const auto rect = QRect{ QPoint(x, y), size() };
 | |
| 	p.drawImage(rect, frame.generated.image);
 | |
| }
 | |
| 
 | |
| void AnimatedIcon::paintInCenter(QPainter &p, QRect rect) {
 | |
| 	const auto my = size();
 | |
| 	paint(
 | |
| 		p,
 | |
| 		rect.x() + (rect.width() - my.width()) / 2,
 | |
| 		rect.y() + (rect.height() - my.height()) / 2);
 | |
| }
 | |
| 
 | |
| void AnimatedIcon::animate(Fn<void()> update) {
 | |
| 	if (framesCount() != 1 && !anim::Disabled()) {
 | |
| 		jumpToStart(std::move(update));
 | |
| 		_animationDuration = _impl->animationDuration();
 | |
| 		_animationCurrentStart = _animationStarted = crl::now();
 | |
| 		continueAnimation(_animationCurrentStart);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void AnimatedIcon::continueAnimation(crl::time now) {
 | |
| 	const auto callback = [=](float64 value) {
 | |
| 		if (anim::Disabled()) {
 | |
| 			return;
 | |
| 		}
 | |
| 		const auto elapsed = int(value);
 | |
| 		const auto now = _animationStartTime + elapsed;
 | |
| 		if (!_animationDuration && elapsed > kDefaultDuration / 2) {
 | |
| 			auto animation = std::move(_animation);
 | |
| 			continueAnimation(now);
 | |
| 		}
 | |
| 		preloadNextFrame(now);
 | |
| 		if (_repaint) _repaint();
 | |
| 	};
 | |
| 	const auto duration = _animationDuration
 | |
| 		? _animationDuration
 | |
| 		: kDefaultDuration;
 | |
| 	_animationStartTime = now;
 | |
| 	_animation.start(callback, 0., 1. * duration, duration);
 | |
| }
 | |
| 
 | |
| void AnimatedIcon::jumpToStart(Fn<void()> update) {
 | |
| 	_repaint = std::move(update);
 | |
| 	_animation.stop();
 | |
| 	_animationCurrentIndex = 0;
 | |
| 	_impl->moveToFrame(0, QSize());
 | |
| }
 | |
| 
 | |
| void AnimatedIcon::frameJumpFinished() {
 | |
| 	if (_repaint && !animating()) {
 | |
| 		_repaint();
 | |
| 		_repaint = nullptr;
 | |
| 	}
 | |
| }
 | |
| 
 | |
| int AnimatedIcon::wantedFrameIndex(
 | |
| 		crl::time now,
 | |
| 		const Frame *resolvedCurrent) const {
 | |
| 	const auto frame = resolvedCurrent ? resolvedCurrent : &_impl->frame();
 | |
| 	if (frame->index == _animationCurrentIndex + 1) {
 | |
| 		++_animationCurrentIndex;
 | |
| 		_animationCurrentStart = _animationNextStart;
 | |
| 	}
 | |
| 	if (!_animation.animating()) {
 | |
| 		return _animationCurrentIndex;
 | |
| 	}
 | |
| 	if (frame->index == _animationCurrentIndex) {
 | |
| 		const auto duration = frame->generated.duration;
 | |
| 		const auto next = _animationCurrentStart + duration;
 | |
| 		if (frame->generated.last) {
 | |
| 			_animation.stop();
 | |
| 			if (_repaint) _repaint();
 | |
| 			return _animationCurrentIndex;
 | |
| 		} else if (now < next) {
 | |
| 			return _animationCurrentIndex;
 | |
| 		}
 | |
| 		_animationNextStart = next;
 | |
| 		return _animationCurrentIndex + 1;
 | |
| 	}
 | |
| 	Assert(!_animationCurrentIndex);
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| void AnimatedIcon::preloadNextFrame(
 | |
| 		crl::time now,
 | |
| 		const Frame *resolvedCurrent,
 | |
| 		QSize updatedDesiredSize) const {
 | |
| 	_impl->moveToFrame(
 | |
| 		wantedFrameIndex(now, resolvedCurrent),
 | |
| 		updatedDesiredSize);
 | |
| }
 | |
| 
 | |
| bool AnimatedIcon::animating() const {
 | |
| 	return _animation.animating();
 | |
| }
 | |
| 
 | |
| std::unique_ptr<AnimatedIcon> MakeAnimatedIcon(
 | |
| 		AnimatedIconDescriptor &&descriptor) {
 | |
| 	return std::make_unique<AnimatedIcon>(std::move(descriptor));
 | |
| }
 | |
| 
 | |
| } // namespace Lottie
 | 
