From 3287bf45c607e980268090ae27dfe4fb69d037e4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 29 Aug 2022 16:32:57 +0400 Subject: [PATCH] Add FrameGenerator-based CustomEmoji and AnimatedIcon. --- CMakeLists.txt | 5 + ui/animated_icon.cpp | 369 ++++++++++++++ ui/animated_icon.h | 90 ++++ ui/effects/frame_generator.cpp | 33 +- ui/effects/frame_generator.h | 26 +- ui/text/custom_emoji_instance.cpp | 775 ++++++++++++++++++++++++++++++ ui/text/custom_emoji_instance.h | 273 +++++++++++ 7 files changed, 1564 insertions(+), 7 deletions(-) create mode 100644 ui/animated_icon.cpp create mode 100644 ui/animated_icon.h create mode 100644 ui/text/custom_emoji_instance.cpp create mode 100644 ui/text/custom_emoji_instance.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0488eeb..8352b47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,6 +149,8 @@ PRIVATE ui/style/style_core_types.h ui/style/style_palette_colorizer.cpp ui/style/style_palette_colorizer.h + ui/text/custom_emoji_instance.cpp + ui/text/custom_emoji_instance.h ui/text/text.cpp ui/text/text.h ui/text/text_block.cpp @@ -236,6 +238,8 @@ PRIVATE ui/wrap/wrap.h ui/abstract_button.cpp ui/abstract_button.h + ui/animated_icon.cpp + ui/animated_icon.h ui/basic_click_handlers.cpp ui/basic_click_handlers.h ui/cached_special_layer_shadow_corners.cpp @@ -308,4 +312,5 @@ PUBLIC PRIVATE desktop-app::external_zlib desktop-app::external_jpeg + desktop-app::external_lz4 ) diff --git a/ui/animated_icon.cpp b/ui/animated_icon.cpp new file mode 100644 index 0000000..189a562 --- /dev/null +++ b/ui/animated_icon.cpp @@ -0,0 +1,369 @@ +/* +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 +#include +#include +#include + +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 { +public: + explicit Impl(base::weak_ptr weak); + + void prepareFromAsync( + FnMut()> factory, + QSize sizeOverride); + void waitTillPrepared() const; + + [[nodiscard]] bool valid() const; + [[nodiscard]] QSize size() const; + [[nodiscard]] int framesCount() 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 _generator; + Frame _current; + QSize _desiredSize; + std::atomic _preloadState = PreloadState::None; + + Frame _preloaded; // Changed on main or async depending on _preloadState. + QSize _preloadImageSize; + + base::weak_ptr _weak; + int _framesCount = 0; + mutable crl::semaphore _semaphore; + mutable bool _ready = false; + +}; + +AnimatedIcon::Impl::Impl(base::weak_ptr weak) +: _weak(weak) { +} + +void AnimatedIcon::Impl::prepareFromAsync( + FnMut()> 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(); + _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; +} + +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(base::make_weak(this))) { + 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(); +} + +QImage AnimatedIcon::frame() const { + return frame(QSize(), nullptr).image; +} + +AnimatedIcon::ResizedFrame AnimatedIcon::frame( + QSize desiredSize, + Fn 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 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 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(); + 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 MakeAnimatedIcon( + AnimatedIconDescriptor &&descriptor) { + return std::make_unique(std::move(descriptor)); +} + +} // namespace Lottie diff --git a/ui/animated_icon.h b/ui/animated_icon.h new file mode 100644 index 0000000..80755f9 --- /dev/null +++ b/ui/animated_icon.h @@ -0,0 +1,90 @@ +/* +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 +*/ +#pragma once + +#include "ui/style/style_core_types.h" +#include "ui/effects/animations.h" +#include "base/weak_ptr.h" + +#include +#include +#include + +namespace Ui { + +class FrameGenerator; + +struct AnimatedIconDescriptor { + FnMut()> generator; + QSize sizeOverride; +}; + +class AnimatedIcon final : public base::has_weak_ptr { +public: + explicit AnimatedIcon(AnimatedIconDescriptor &&descriptor); + AnimatedIcon(const AnimatedIcon &other) = delete; + AnimatedIcon &operator=(const AnimatedIcon &other) = delete; + AnimatedIcon(AnimatedIcon &&other) = delete; // _animation captures this. + AnimatedIcon &operator=(AnimatedIcon &&other) = delete; + + [[nodiscard]] bool valid() const; + [[nodiscard]] int frameIndex() const; + [[nodiscard]] int framesCount() const; + [[nodiscard]] QImage frame() const; + [[nodiscard]] int width() const; + [[nodiscard]] int height() const; + [[nodiscard]] QSize size() const; + + struct ResizedFrame { + QImage image; + bool scaled = false; + }; + [[nodiscard]] ResizedFrame frame( + QSize desiredSize, + Fn updateWithPerfect) const; + + void animate(Fn update); + void jumpToStart(Fn update); + + void paint(QPainter &p, int x, int y); + void paintInCenter(QPainter &p, QRect rect); + + [[nodiscard]] bool animating() const; + +private: + struct Frame; + class Impl; + friend class Impl; + + void wait() const; + [[nodiscard]] int wantedFrameIndex( + crl::time now, + const Frame *resolvedCurrent = nullptr) const; + void preloadNextFrame( + crl::time now, + const Frame *resolvedCurrent = nullptr, + QSize updatedDesiredSize = QSize()) const; + void frameJumpFinished(); + void continueAnimation(crl::time now); + + std::shared_ptr _impl; + crl::time _animationStartTime = 0; + crl::time _animationStarted = 0; + mutable Animations::Simple _animation; + mutable Fn _repaint; + mutable crl::time _animationDuration = 0; + mutable crl::time _animationCurrentStart = 0; + mutable crl::time _animationNextStart = 0; + mutable int _animationCurrentIndex = 0; + +}; + +[[nodiscard]] std::unique_ptr MakeAnimatedIcon( + AnimatedIconDescriptor &&descriptor); + +} // namespace Ui diff --git a/ui/effects/frame_generator.cpp b/ui/effects/frame_generator.cpp index 43a20d4..a50b443 100644 --- a/ui/effects/frame_generator.cpp +++ b/ui/effects/frame_generator.cpp @@ -14,21 +14,39 @@ ImageFrameGenerator::ImageFrameGenerator(const QByteArray &bytes) : _bytes(bytes) { } +ImageFrameGenerator::ImageFrameGenerator(const QImage &image) +: _image(image) { +} + int ImageFrameGenerator::count() { return 1; } +double ImageFrameGenerator::rate() { + return 1.; +} + FrameGenerator::Frame ImageFrameGenerator::renderNext( QImage storage, QSize size, Qt::AspectRatioMode mode) { - storage = Images::Read({ - .content = _bytes, - }).image; - if (storage.isNull()) { + return renderCurrent(std::move(storage), size, mode); +} + +FrameGenerator::Frame ImageFrameGenerator::renderCurrent( + QImage storage, + QSize size, + Qt::AspectRatioMode mode) { + if (_image.isNull() && !_bytes.isEmpty()) { + _image = Images::Read({ + .content = _bytes, + }).image; + _bytes = QByteArray(); + } + if (_image.isNull()) { return {}; } - auto scaled = storage.scaled( + auto scaled = _image.scaled( size, mode, Qt::SmoothTransformation @@ -52,7 +70,10 @@ FrameGenerator::Frame ImageFrameGenerator::renderNext( dst += dstPerLine; } - return { .image = std::move(result) }; + return { .image = std::move(result), .last = true }; +} + +void ImageFrameGenerator::jumpToStart() { } } // namespace Ui diff --git a/ui/effects/frame_generator.h b/ui/effects/frame_generator.h index cf413dc..7c6a45f 100644 --- a/ui/effects/frame_generator.h +++ b/ui/effects/frame_generator.h @@ -15,31 +15,55 @@ namespace Ui { class FrameGenerator { public: virtual ~FrameGenerator() = default; + + // 0 means unknown. [[nodiscard]] virtual int count() = 0; + // 0. means unknown. + [[nodiscard]] virtual double rate() = 0; + struct Frame { - QImage image; crl::time duration = 0; + QImage image; + bool last = false; }; [[nodiscard]] virtual Frame renderNext( QImage storage, QSize size, Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) = 0; + [[nodiscard]] virtual Frame renderCurrent( + QImage storage, + QSize size, + Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) = 0; + + virtual void jumpToStart() = 0; + }; class ImageFrameGenerator final : public Ui::FrameGenerator { public: explicit ImageFrameGenerator(const QByteArray &bytes); + explicit ImageFrameGenerator(const QImage &image); int count() override; + double rate() override; Frame renderNext( QImage storage, QSize size, Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override; + Frame renderCurrent( + QImage storage, + QSize size, + Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override; + void jumpToStart() override; private: QByteArray _bytes; + QImage _image; }; +[[nodiscard]] bool GoodStorageForFrame(const QImage &storage, QSize size); +[[nodiscard]] QImage CreateFrameStorage(QSize size); + } // namespace Ui diff --git a/ui/text/custom_emoji_instance.cpp b/ui/text/custom_emoji_instance.cpp new file mode 100644 index 0000000..928691a --- /dev/null +++ b/ui/text/custom_emoji_instance.cpp @@ -0,0 +1,775 @@ +/* +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/text/custom_emoji_instance.h" + +#include "ui/effects/animation_value.h" +#include "ui/effects/frame_generator.h" +#include "ui/ui_utility.h" +#include "ui/painter.h" + +#include +#include + +class QPainter; + +namespace Ui::CustomEmoji { +namespace { + +constexpr auto kMaxSize = 128; +constexpr auto kMaxFrames = 180; +constexpr auto kCacheVersion = 1; +constexpr auto kPreloadFrames = 3; + +struct CacheHeader { + int version = 0; + int size = 0; + int frames = 0; + int length = 0; +}; + +void PaintScaledImage( + QPainter &p, + const QRect &target, + const Cache::Frame &frame, + const Context &context) { + if (context.scaled) { + const auto sx = anim::interpolate( + target.width() / 2, + 0, + context.scale); + const auto sy = (target.height() == target.width()) + ? sx + : anim::interpolate(target.height() / 2, 0, context.scale); + const auto scaled = target.marginsRemoved({ sx, sy, sx, sy }); + if (frame.source.isNull()) { + p.drawImage(scaled, *frame.image); + } else { + p.drawImage(scaled, *frame.image, frame.source); + } + } else if (frame.source.isNull()) { + p.drawImage(target, *frame.image); + } else { + p.drawImage(target, *frame.image, frame.source); + } +} + +} // namespace + +Preview::Preview(QPainterPath path, float64 scale) +: _data(ScaledPath{ std::move(path), scale }) { +} + +Preview::Preview(QImage image, bool exact) +: _data(Image{ .data = std::move(image), .exact = exact }) { +} + +void Preview::paint(QPainter &p, const Context &context) { + if (const auto path = std::get_if(&_data)) { + paintPath(p, context, *path); + } else if (const auto image = std::get_if(&_data)) { + const auto &data = image->data; + const auto factor = style::DevicePixelRatio(); + const auto rect = QRect(context.position, data.size() / factor); + PaintScaledImage(p, rect, { .image = &data }, context); + } +} + +bool Preview::isImage() const { + return v::is(_data); +} + +bool Preview::isExactImage() const { + if (const auto image = std::get_if(&_data)) { + return image->exact; + } + return false; +} + +QImage Preview::image() const { + if (const auto image = std::get_if(&_data)) { + return image->data; + } + return QImage(); +} + +void Preview::paintPath( + QPainter &p, + const Context &context, + const ScaledPath &path) { + auto hq = PainterHighQualityEnabler(p); + p.setBrush(context.preview); + p.setPen(Qt::NoPen); + const auto scale = path.scale; + const auto required = (scale != 1.) || context.scaled; + if (required) { + p.save(); + } + p.translate(context.position); + if (required) { + p.scale(scale, scale); + const auto center = QPoint( + context.size.width() / 2, + context.size.height() / 2); + if (context.scaled) { + p.translate(center); + p.scale(context.scale, context.scale); + p.translate(-center); + } + } + p.drawPath(path.path); + if (required) { + p.restore(); + } else { + p.translate(-context.position); + } +} + +Cache::Cache(int size) : _size(size) { +} + +std::optional Cache::FromSerialized( + const QByteArray &serialized, + int requestedSize) { + Expects(requestedSize > 0 && requestedSize <= kMaxSize); + + if (serialized.size() <= sizeof(CacheHeader)) { + return {}; + } + auto header = CacheHeader(); + memcpy(&header, serialized.data(), sizeof(header)); + const auto size = header.size; + if (size != requestedSize + || header.frames <= 0 + || header.frames >= kMaxFrames + || header.length <= 0 + || header.length > (size * size * header.frames * sizeof(int32)) + || (serialized.size() != sizeof(CacheHeader) + + header.length + + (header.frames * sizeof(Cache(0)._durations[0])))) { + return {}; + } + const auto rows = (header.frames + kPerRow - 1) / kPerRow; + const auto columns = std::min(header.frames, kPerRow); + auto durations = std::vector(header.frames, 0); + auto full = QImage( + columns * size, + rows * size, + QImage::Format_ARGB32_Premultiplied); + Assert(full.bytesPerLine() == full.width() * sizeof(int32)); + + const auto decompressed = LZ4_decompress_safe( + serialized.data() + sizeof(CacheHeader), + reinterpret_cast(full.bits()), + header.length, + full.bytesPerLine() * full.height()); + if (decompressed <= 0) { + return {}; + } + memcpy( + durations.data(), + serialized.data() + sizeof(CacheHeader) + header.length, + header.frames * sizeof(durations[0])); + + auto result = Cache(size); + result._finished = true; + result._full = std::move(full); + result._frames = header.frames; + result._durations = std::move(durations); + return result; +} + +QByteArray Cache::serialize() { + Expects(_finished); + Expects(_durations.size() == _frames); + Expects(_full.bytesPerLine() == sizeof(int32) * _full.width()); + + auto header = CacheHeader{ + .version = kCacheVersion, + .size = _size, + .frames = _frames, + }; + const auto input = _full.width() * _full.height() * sizeof(int32); + const auto max = sizeof(CacheHeader) + + LZ4_compressBound(input) + + (_frames * sizeof(_durations[0])); + auto result = QByteArray(max, Qt::Uninitialized); + header.length = LZ4_compress_default( + reinterpret_cast(_full.constBits()), + result.data() + sizeof(CacheHeader), + input, + result.size() - sizeof(CacheHeader)); + Assert(header.length > 0); + memcpy(result.data(), &header, sizeof(CacheHeader)); + memcpy( + result.data() + sizeof(CacheHeader) + header.length, + _durations.data(), + _frames * sizeof(_durations[0])); + result.resize(sizeof(CacheHeader) + + header.length + + _frames * sizeof(_durations[0])); + + return result; +} + +int Cache::frames() const { + return _frames; +} + +Cache::Frame Cache::frame(int index) const { + Expects(index < _frames); + + const auto row = index / kPerRow; + const auto inrow = index % kPerRow; + if (_finished) { + return { &_full, { inrow * _size, row * _size, _size, _size } }; + } + return { &_images[row], { 0, inrow * _size, _size, _size } }; +} + +int Cache::size() const { + return _size; +} + +Preview Cache::makePreview() const { + Expects(_frames > 0); + + const auto first = frame(0); + return { first.image->copy(first.source), true }; +} + +void Cache::reserve(int frames) { + Expects(!_finished); + + const auto rows = (frames + kPerRow - 1) / kPerRow; + if (const auto add = rows - int(_images.size()); add > 0) { + _images.resize(rows); + for (auto e = end(_images), i = e - add; i != e; ++i) { + (*i) = QImage( + _size, + _size * kPerRow, + QImage::Format_ARGB32_Premultiplied); + } + } + _durations.reserve(frames); +} + +int Cache::frameRowByteSize() const { + return _size * 4; +} + +int Cache::frameByteSize() const { + return _size * frameRowByteSize(); +} + +void Cache::add(crl::time duration, const QImage &frame) { + Expects(!_finished); + Expects(frame.size() == QSize(_size, _size)); + Expects(frame.format() == QImage::Format_ARGB32_Premultiplied); + + const auto row = (_frames / kPerRow); + const auto inrow = (_frames % kPerRow); + const auto rows = row + 1; + while (_images.size() < rows) { + _images.emplace_back(); + _images.back() = QImage( + _size, + _size * kPerRow, + QImage::Format_ARGB32_Premultiplied); + } + const auto srcPerLine = frame.bytesPerLine(); + const auto dstPerLine = _images[row].bytesPerLine(); + const auto perLine = std::min(srcPerLine, dstPerLine); + auto dst = _images[row].bits() + inrow * _size * dstPerLine; + auto src = frame.constBits(); + for (auto y = 0; y != _size; ++y) { + memcpy(dst, src, perLine); + dst += dstPerLine; + src += srcPerLine; + } + ++_frames; + _durations.push_back(std::clamp( + duration, + crl::time(0), + crl::time(std::numeric_limits::max()))); +} + +void Cache::finish() { + _finished = true; + if (_frame == _frames) { + _frame = 0; + } + const auto rows = (_frames + kPerRow - 1) / kPerRow; + const auto columns = std::min(_frames, kPerRow); + const auto zero = (rows * columns) - _frames; + _full = QImage( + columns * _size, + rows * _size, + QImage::Format_ARGB32_Premultiplied); + auto dstData = _full.bits(); + const auto perLine = _size * 4; + const auto dstPerLine = _full.bytesPerLine(); + for (auto y = 0; y != rows; ++y) { + auto &row = _images[y]; + auto src = row.bits(); + const auto srcPerLine = row.bytesPerLine(); + const auto till = columns - ((y + 1 == rows) ? zero : 0); + for (auto x = 0; x != till; ++x) { + auto dst = dstData + y * dstPerLine * _size + x * perLine; + for (auto line = 0; line != _size; ++line) { + memcpy(dst, src, perLine); + src += srcPerLine; + dst += dstPerLine; + } + } + } + if (const auto perLine = zero * _size) { + auto dst = dstData + + (rows - 1) * dstPerLine * _size + + (columns - zero) * _size * 4; + for (auto left = 0; left != _size; ++left) { + memset(dst, 0, perLine); + dst += dstPerLine; + } + } +} + +PaintFrameResult Cache::paintCurrentFrame( + QPainter &p, + const Context &context) { + if (!_frames) { + return {}; + } + const auto first = context.firstFrameOnly; + if (!first) { + const auto now = context.paused ? 0 : context.now; + const auto finishes = now ? currentFrameFinishes() : 0; + if (finishes && now >= finishes) { + ++_frame; + if (_finished && _frame == _frames) { + _frame = 0; + } + _shown = now; + } else if (!_shown) { + _shown = now; + } + } + const auto index = first ? 0 : std::min(_frame, _frames - 1); + const auto info = frame(index); + const auto size = _size / style::DevicePixelRatio(); + const auto rect = QRect(context.position, QSize(size, size)); + PaintScaledImage(p, rect, info, context); + const auto next = first ? 0 : currentFrameFinishes(); + return { + .painted = true, + .next = next, + .duration = next ? (next - _shown) : 0, + }; +} + +int Cache::currentFrame() const { + return _frame; +} + +crl::time Cache::currentFrameFinishes() const { + if (!_shown || _frame >= _durations.size()) { + return 0; + } else if (const auto duration = _durations[_frame]) { + return _shown + duration; + } + return 0; +} + +Cached::Cached( + const QString &entityData, + Fn()> unloader, + Cache cache) +: _unloader(std::move(unloader)) +, _cache(std::move(cache)) +, _entityData(entityData) { +} + +QString Cached::entityData() const { + return _entityData; +} + +PaintFrameResult Cached::paint(QPainter &p, const Context &context) { + return _cache.paintCurrentFrame(p, context); +} + +Preview Cached::makePreview() const { + return _cache.makePreview(); +} + +Loading Cached::unload() { + return Loading(_unloader(), makePreview()); +} + +Renderer::Renderer(RendererDescriptor &&descriptor) +: _cache(descriptor.size) +, _put(std::move(descriptor.put)) +, _loader(std::move(descriptor.loader)) { + Expects(_loader != nullptr); + + const auto size = _cache.size(); + const auto guard = base::make_weak(this); + crl::async([=, factory = std::move(descriptor.generator)]() mutable { + auto generator = factory(); + auto rendered = generator->renderNext( + QImage(), + QSize(size, size), + Qt::KeepAspectRatio); + if (rendered.image.isNull()) { + return; + } + crl::on_main(guard, [ + =, + frame = std::move(rendered), + generator = std::move(generator) + ]() mutable { + frameReady( + std::move(generator), + frame.duration, + std::move(frame.image)); + }); + }); +} + +Renderer::~Renderer() = default; + +void Renderer::frameReady( + std::unique_ptr generator, + crl::time duration, + QImage frame) { + if (frame.isNull()) { + finish(); + return; + } + if (const auto count = generator->count()) { + if (!_cache.frames()) { + _cache.reserve(std::max(count, kMaxFrames)); + } + } + const auto current = _cache.currentFrame(); + const auto total = _cache.frames(); + const auto explicitRepaint = (current == total); + _cache.add(duration, frame); + if (explicitRepaint && _repaint) { + _repaint(); + } + if (!duration || total + 1 >= kMaxFrames) { + finish(); + } else if (current + kPreloadFrames > total) { + renderNext(std::move(generator), std::move(frame)); + } else { + _generator = std::move(generator); + _storage = std::move(frame); + } +} + +void Renderer::renderNext( + std::unique_ptr generator, + QImage storage) { + const auto size = _cache.size(); + const auto guard = base::make_weak(this); + crl::async([ + =, + storage = std::move(storage), + generator = std::move(generator) + ]() mutable { + auto rendered = generator->renderNext( + std::move(storage), + QSize(size, size), + Qt::KeepAspectRatio); + crl::on_main(guard, [ + =, + frame = std::move(rendered), + generator = std::move(generator) + ]() mutable { + frameReady( + std::move(generator), + frame.duration, + std::move(frame.image)); + }); + }); +} + +void Renderer::finish() { + _finished = true; + _cache.finish(); + if (_put) { + _put(_cache.serialize()); + } +} + +PaintFrameResult Renderer::paint(QPainter &p, const Context &context) { + const auto result = _cache.paintCurrentFrame(p, context); + if (_generator + && (!result.painted + || _cache.currentFrame() + kPreloadFrames >= _cache.frames())) { + renderNext(std::move(_generator), std::move(_storage)); + } + return result; +} + +std::optional Renderer::ready(const QString &entityData) { + return _finished + ? Cached{ entityData, std::move(_loader), std::move(_cache) } + : std::optional(); +} + +std::unique_ptr Renderer::cancel() { + return _loader(); +} + +bool Renderer::canMakePreview() const { + return _cache.frames() > 0; +} + +Preview Renderer::makePreview() const { + return _cache.makePreview(); +} + +void Renderer::setRepaintCallback(Fn repaint) { + _repaint = std::move(repaint); +} + +Cache Renderer::takeCache() { + return std::move(_cache); +} + +Loading::Loading(std::unique_ptr loader, Preview preview) +: _loader(std::move(loader)) +, _preview(std::move(preview)) { +} + +QString Loading::entityData() const { + return _loader->entityData(); +} + +void Loading::load(Fn done) { + _loader->load(crl::guard(this, [this, done = std::move(done)]( + Loader::LoadResult result) mutable { + if (const auto caching = std::get_if(&result)) { + caching->preview = _preview + ? std::move(_preview) + : _loader->preview(); + } + done(std::move(result)); + })); +} + +bool Loading::loading() const { + return _loader->loading(); +} + +void Loading::paint(QPainter &p, const Context &context) { + if (!_preview) { + if (auto preview = _loader->preview()) { + _preview = std::move(preview); + } + } + _preview.paint(p, context); +} + +bool Loading::hasImagePreview() const { + return _preview.isImage(); +} + +Preview Loading::imagePreview() const { + return _preview.isImage() ? _preview : Preview(); +} + +void Loading::updatePreview(Preview preview) { + if (!_preview.isImage() && preview.isImage()) { + _preview = std::move(preview); + } else if (!_preview) { + if (auto loaderPreview = _loader->preview()) { + _preview = std::move(loaderPreview); + } else if (preview) { + _preview = std::move(preview); + } + } +} + +void Loading::cancel() { + _loader->cancel(); + invalidate_weak_ptrs(this); +} + +Instance::Instance( + Loading loading, + Fn, RepaintRequest)> repaintLater) +: _state(std::move(loading)) +, _repaintLater(std::move(repaintLater)) { +} + +QString Instance::entityData() const { + return v::match(_state, [](const Loading &state) { + return state.entityData(); + }, [](const Caching &state) { + return state.entityData; + }, [](const Cached &state) { + return state.entityData(); + }); +} + +void Instance::paint(QPainter &p, const Context &context) { + v::match(_state, [&](Loading &state) { + state.paint(p, context); + load(state); + }, [&](Caching &state) { + auto result = state.renderer->paint(p, context); + if (!result.painted) { + state.preview.paint(p, context); + } else { + if (!state.preview.isExactImage()) { + state.preview = state.renderer->makePreview(); + } + if (result.next > context.now) { + _repaintLater(this, { result.next, result.duration }); + } + } + if (auto cached = state.renderer->ready(state.entityData)) { + _state = std::move(*cached); + } + }, [&](Cached &state) { + const auto result = state.paint(p, context); + if (result.next > context.now) { + _repaintLater(this, { result.next, result.duration }); + } + }); +} + +bool Instance::ready() { + return v::match(_state, [&](Loading &state) { + if (state.hasImagePreview()) { + return true; + } + load(state); + return false; + }, [](Caching &state) { + return state.renderer->canMakePreview(); + }, [](Cached &state) { + return true; + }); +} + +void Instance::load(Loading &state) { + state.load([=](Loader::LoadResult result) { + if (auto caching = std::get_if(&result)) { + caching->renderer->setRepaintCallback([=] { repaint(); }); + _state = std::move(*caching); + } else if (auto cached = std::get_if(&result)) { + _state = std::move(*cached); + repaint(); + } else { + Unexpected("Value in Loader::LoadResult."); + } + }); +} + +bool Instance::hasImagePreview() const { + return v::match(_state, [](const Loading &state) { + return state.hasImagePreview(); + }, [](const Caching &state) { + return state.preview.isImage(); + }, [](const Cached &state) { + return true; + }); +} + +Preview Instance::imagePreview() const { + return v::match(_state, [](const Loading &state) { + return state.imagePreview(); + }, [](const Caching &state) { + return state.preview.isImage() ? state.preview : Preview(); + }, [](const Cached &state) { + return state.makePreview(); + }); +} + +void Instance::updatePreview(Preview preview) { + v::match(_state, [&](Loading &state) { + state.updatePreview(std::move(preview)); + }, [&](Caching &state) { + if ((!state.preview.isImage() && preview.isImage()) + || (!state.preview && preview)) { + state.preview = std::move(preview); + } + }, [](const Cached &) {}); +} + +void Instance::repaint() { + for (const auto &object : _usage) { + object->repaint(); + } +} + +void Instance::incrementUsage(not_null object) { + _usage.emplace(object); +} + +void Instance::decrementUsage(not_null object) { + _usage.remove(object); + if (!_usage.empty()) { + return; + } + v::match(_state, [](Loading &state) { + state.cancel(); + }, [&](Caching &state) { + _state = Loading{ + state.renderer->cancel(), + std::move(state.preview), + }; + }, [&](Cached &state) { + _state = state.unload(); + }); + _repaintLater(this, RepaintRequest()); +} + +Object::Object(not_null instance, Fn repaint) +: _instance(instance) +, _repaint(std::move(repaint)) { +} + +Object::~Object() { + unload(); +} + +QString Object::entityData() { + return _instance->entityData(); +} + +void Object::paint(QPainter &p, const Context &context) { + if (!_using) { + _using = true; + _instance->incrementUsage(this); + } + _instance->paint(p, context); +} + +void Object::unload() { + if (_using) { + _using = false; + _instance->decrementUsage(this); + } +} + +bool Object::ready() { + if (!_using) { + _using = true; + _instance->incrementUsage(this); + } + return _instance->ready(); +} + +void Object::repaint() { + _repaint(); +} + +} // namespace Ui::CustomEmoji diff --git a/ui/text/custom_emoji_instance.h b/ui/text/custom_emoji_instance.h new file mode 100644 index 0000000..6efb6d1 --- /dev/null +++ b/ui/text/custom_emoji_instance.h @@ -0,0 +1,273 @@ +/* +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 +*/ +#pragma once + +#include "ui/text/text_custom_emoji.h" +#include "base/weak_ptr.h" +#include "base/bytes.h" +#include "base/timer.h" + +#include + +class QColor; +class QPainter; + +namespace Ui { +class FrameGenerator; +} // namespace Ui + +namespace Ui::CustomEmoji { + +using Context = Ui::Text::CustomEmoji::Context; + +class Preview final { +public: + Preview() = default; + Preview(QImage image, bool exact); + Preview(QPainterPath path, float64 scale); + + void paint(QPainter &p, const Context &context); + [[nodiscard]] bool isImage() const; + [[nodiscard]] bool isExactImage() const; + [[nodiscard]] QImage image() const; + + [[nodiscard]] explicit operator bool() const { + return !v::is_null(_data); + } + +private: + struct ScaledPath { + QPainterPath path; + float64 scale = 1.; + }; + struct Image { + QImage data; + bool exact = false; + }; + + void paintPath( + QPainter &p, + const Context &context, + const ScaledPath &path); + + std::variant _data; + +}; + +struct PaintFrameResult { + bool painted = false; + crl::time next = 0; + crl::time duration = 0; +}; + +class Cache final { +public: + Cache(int size); + + struct Frame { + not_null image; + QRect source; + }; + + [[nodiscard]] static std::optional FromSerialized( + const QByteArray &serialized, + int requestedSize); + [[nodiscard]] QByteArray serialize(); + + [[nodiscard]] int size() const; + [[nodiscard]] int frames() const; + [[nodiscard]] Frame frame(int index) const; + void reserve(int frames); + void add(crl::time duration, const QImage &frame); + void finish(); + + [[nodiscard]] Preview makePreview() const; + + PaintFrameResult paintCurrentFrame(QPainter &p, const Context &context); + [[nodiscard]] int currentFrame() const; + +private: + static constexpr auto kPerRow = 16; + + [[nodiscard]] int frameRowByteSize() const; + [[nodiscard]] int frameByteSize() const; + [[nodiscard]] crl::time currentFrameFinishes() const; + + std::vector _images; + std::vector _durations; + QImage _full; + crl::time _shown = 0; + int _frame = 0; + int _size = 0; + int _frames = 0; + bool _finished = false; + +}; + +class Loader; +class Loading; + +class Cached final { +public: + Cached( + const QString &entityData, + Fn()> unloader, + Cache cache); + + [[nodiscard]] QString entityData() const; + [[nodiscard]] Preview makePreview() const; + PaintFrameResult paint(QPainter &p, const Context &context); + [[nodiscard]] Loading unload(); + +private: + Fn()> _unloader; + Cache _cache; + QString _entityData; + +}; + +struct RendererDescriptor { + Fn()> generator; + Fn put; + Fn()> loader; + int size = 0; +}; + +class Renderer final : public base::has_weak_ptr { +public: + explicit Renderer(RendererDescriptor &&descriptor); + virtual ~Renderer(); + + PaintFrameResult paint(QPainter &p, const Context &context); + [[nodiscard]] std::optional ready(const QString &entityData); + [[nodiscard]] std::unique_ptr cancel(); + + [[nodiscard]] bool canMakePreview() const; + [[nodiscard]] Preview makePreview() const; + + void setRepaintCallback(Fn repaint); + [[nodiscard]] Cache takeCache(); + +private: + void frameReady( + std::unique_ptr generator, + crl::time duration, + QImage frame); + void renderNext( + std::unique_ptr generator, + QImage storage); + void finish(); + + Cache _cache; + std::unique_ptr _generator; + QImage _storage; + Fn _put; + Fn _repaint; + Fn()> _loader; + bool _finished = false; + +}; + +struct Caching { + std::unique_ptr renderer; + QString entityData; + Preview preview; +}; + +class Loader { +public: + using LoadResult = std::variant; + [[nodiscard]] virtual QString entityData() = 0; + virtual void load(Fn loaded) = 0; + [[nodiscard]] virtual bool loading() = 0; + virtual void cancel() = 0; + [[nodiscard]] virtual Preview preview() = 0; + virtual ~Loader() = default; +}; + +class Loading final : public base::has_weak_ptr { +public: + Loading(std::unique_ptr loader, Preview preview); + + [[nodiscard]] QString entityData() const; + + void load(Fn done); + [[nodiscard]] bool loading() const; + void paint(QPainter &p, const Context &context); + [[nodiscard]] bool hasImagePreview() const; + [[nodiscard]] Preview imagePreview() const; + void updatePreview(Preview preview); + void cancel(); + +private: + std::unique_ptr _loader; + Preview _preview; + +}; + +struct RepaintRequest { + crl::time when = 0; + crl::time duration = 0; +}; + +class Object; +class Instance final : public base::has_weak_ptr { +public: + Instance( + Loading loading, + Fn, RepaintRequest)> repaintLater); + Instance(const Instance&) = delete; + Instance &operator=(const Instance&) = delete; + + [[nodiscard]] QString entityData() const; + void paint(QPainter &p, const Context &context); + [[nodiscard]] bool ready(); + [[nodiscard]] bool hasImagePreview() const; + [[nodiscard]] Preview imagePreview() const; + void updatePreview(Preview preview); + + void incrementUsage(not_null object); + void decrementUsage(not_null object); + + void repaint(); + +private: + void load(Loading &state); + + std::variant _state; + base::flat_set> _usage; + Fn that, RepaintRequest)> _repaintLater; + +}; + +class Delegate { +public: + [[nodiscard]] virtual bool paused() = 0; + virtual ~Delegate() = default; +}; + +class Object final : public Ui::Text::CustomEmoji { +public: + Object(not_null instance, Fn repaint); + ~Object(); + + QString entityData() override; + void paint(QPainter &p, const Context &context) override; + void unload() override; + bool ready() override; + + void repaint(); + +private: + const not_null _instance; + Fn _repaint; + bool _using = false; + +}; + +} // namespace Ui::CustomEmoji