Add FrameGenerator-based CustomEmoji and AnimatedIcon.

This commit is contained in:
John Preston 2022-08-29 16:32:57 +04:00
parent f876d15eed
commit 3287bf45c6
7 changed files with 1564 additions and 7 deletions

View file

@ -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
)

369
ui/animated_icon.cpp Normal file
View file

@ -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 <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]] 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;
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();
_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<Impl>(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<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();
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

90
ui/animated_icon.h Normal file
View file

@ -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 <crl/crl_time.h>
#include <QtCore/QByteArray>
#include <optional>
namespace Ui {
class FrameGenerator;
struct AnimatedIconDescriptor {
FnMut<std::unique_ptr<FrameGenerator>()> 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<void()> updateWithPerfect) const;
void animate(Fn<void()> update);
void jumpToStart(Fn<void()> 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> _impl;
crl::time _animationStartTime = 0;
crl::time _animationStarted = 0;
mutable Animations::Simple _animation;
mutable Fn<void()> _repaint;
mutable crl::time _animationDuration = 0;
mutable crl::time _animationCurrentStart = 0;
mutable crl::time _animationNextStart = 0;
mutable int _animationCurrentIndex = 0;
};
[[nodiscard]] std::unique_ptr<AnimatedIcon> MakeAnimatedIcon(
AnimatedIconDescriptor &&descriptor);
} // namespace Ui

View file

@ -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

View file

@ -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

View file

@ -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 <crl/crl_async.h>
#include <lz4.h>
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<ScaledPath>(&_data)) {
paintPath(p, context, *path);
} else if (const auto image = std::get_if<Image>(&_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<Image>(_data);
}
bool Preview::isExactImage() const {
if (const auto image = std::get_if<Image>(&_data)) {
return image->exact;
}
return false;
}
QImage Preview::image() const {
if (const auto image = std::get_if<Image>(&_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> 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<uint16>(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<char*>(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<const char*>(_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<uint16>::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<std::unique_ptr<Loader>()> 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<Ui::FrameGenerator> 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<Ui::FrameGenerator> 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<Cached> Renderer::ready(const QString &entityData) {
return _finished
? Cached{ entityData, std::move(_loader), std::move(_cache) }
: std::optional<Cached>();
}
std::unique_ptr<Loader> Renderer::cancel() {
return _loader();
}
bool Renderer::canMakePreview() const {
return _cache.frames() > 0;
}
Preview Renderer::makePreview() const {
return _cache.makePreview();
}
void Renderer::setRepaintCallback(Fn<void()> repaint) {
_repaint = std::move(repaint);
}
Cache Renderer::takeCache() {
return std::move(_cache);
}
Loading::Loading(std::unique_ptr<Loader> loader, Preview preview)
: _loader(std::move(loader))
, _preview(std::move(preview)) {
}
QString Loading::entityData() const {
return _loader->entityData();
}
void Loading::load(Fn<void(Loader::LoadResult)> done) {
_loader->load(crl::guard(this, [this, done = std::move(done)](
Loader::LoadResult result) mutable {
if (const auto caching = std::get_if<Caching>(&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<void(not_null<Instance*>, 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<Caching>(&result)) {
caching->renderer->setRepaintCallback([=] { repaint(); });
_state = std::move(*caching);
} else if (auto cached = std::get_if<Cached>(&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*> object) {
_usage.emplace(object);
}
void Instance::decrementUsage(not_null<Object*> 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*> instance, Fn<void()> 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

View file

@ -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 <QtGui/QPainterPath>
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<v::null_t, ScaledPath, Image> _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<const QImage*> image;
QRect source;
};
[[nodiscard]] static std::optional<Cache> 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<QImage> _images;
std::vector<uint16> _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<std::unique_ptr<Loader>()> unloader,
Cache cache);
[[nodiscard]] QString entityData() const;
[[nodiscard]] Preview makePreview() const;
PaintFrameResult paint(QPainter &p, const Context &context);
[[nodiscard]] Loading unload();
private:
Fn<std::unique_ptr<Loader>()> _unloader;
Cache _cache;
QString _entityData;
};
struct RendererDescriptor {
Fn<std::unique_ptr<Ui::FrameGenerator>()> generator;
Fn<void(QByteArray)> put;
Fn<std::unique_ptr<Loader>()> 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<Cached> ready(const QString &entityData);
[[nodiscard]] std::unique_ptr<Loader> cancel();
[[nodiscard]] bool canMakePreview() const;
[[nodiscard]] Preview makePreview() const;
void setRepaintCallback(Fn<void()> repaint);
[[nodiscard]] Cache takeCache();
private:
void frameReady(
std::unique_ptr<Ui::FrameGenerator> generator,
crl::time duration,
QImage frame);
void renderNext(
std::unique_ptr<Ui::FrameGenerator> generator,
QImage storage);
void finish();
Cache _cache;
std::unique_ptr<Ui::FrameGenerator> _generator;
QImage _storage;
Fn<void(QByteArray)> _put;
Fn<void()> _repaint;
Fn<std::unique_ptr<Loader>()> _loader;
bool _finished = false;
};
struct Caching {
std::unique_ptr<Renderer> renderer;
QString entityData;
Preview preview;
};
class Loader {
public:
using LoadResult = std::variant<Caching, Cached>;
[[nodiscard]] virtual QString entityData() = 0;
virtual void load(Fn<void(LoadResult)> 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> loader, Preview preview);
[[nodiscard]] QString entityData() const;
void load(Fn<void(Loader::LoadResult)> 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> _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<void(not_null<Instance*>, 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*> object);
void decrementUsage(not_null<Object*> object);
void repaint();
private:
void load(Loading &state);
std::variant<Loading, Caching, Cached> _state;
base::flat_set<not_null<Object*>> _usage;
Fn<void(not_null<Instance*> 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*> instance, Fn<void()> 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*> _instance;
Fn<void()> _repaint;
bool _using = false;
};
} // namespace Ui::CustomEmoji