Add FrameGenerator-based CustomEmoji and AnimatedIcon.
This commit is contained in:
parent
f876d15eed
commit
3287bf45c6
7 changed files with 1564 additions and 7 deletions
|
|
@ -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
369
ui/animated_icon.cpp
Normal 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
90
ui/animated_icon.h
Normal 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
|
||||
|
|
@ -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({
|
||||
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;
|
||||
if (storage.isNull()) {
|
||||
_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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
775
ui/text/custom_emoji_instance.cpp
Normal file
775
ui/text/custom_emoji_instance.cpp
Normal 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
|
||||
273
ui/text/custom_emoji_instance.h
Normal file
273
ui/text/custom_emoji_instance.h
Normal 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
|
||||
Loading…
Add table
Reference in a new issue