Support optimized lottie emoji.

This commit is contained in:
John Preston 2022-06-28 17:13:20 +04:00
parent 9d280da80b
commit 6db3a0ec98
10 changed files with 831 additions and 213 deletions

View file

@ -27,6 +27,10 @@ constexpr auto kDontCacheLottieAfterArea = 512 * 512;
} // namespace
uint8 LottieCacheKeyShift(uint8 replacementsTag, StickerLottieSize sizeTag) {
return ((replacementsTag << 4) & 0xF0) | (uint8(sizeTag) & 0x0F);
}
template <typename Method>
auto LottieCachedFromContent(
Method &&method,
@ -115,8 +119,9 @@ std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
replacements,
std::move(renderer));
};
const auto tag = replacements ? replacements->tag : uint8(0);
const auto keyShift = ((tag << 4) & 0xF0) | (uint8(sizeTag) & 0x0F);
const auto keyShift = LottieCacheKeyShift(
replacements ? replacements->tag : uint8(0),
sizeTag);
return LottieFromDocument(method, media, uint8(keyShift), box);
}

View file

@ -49,7 +49,7 @@ using StickersSetFlags = base::flags<StickersSetFlag>;
namespace ChatHelpers {
enum class StickerLottieSize : uchar {
enum class StickerLottieSize : uint8 {
MessageHistory,
StickerSet,
StickersPanel,
@ -66,6 +66,9 @@ enum class StickerLottieSize : uchar {
EmojiInteractionReserved7,
PremiumReactionPreview,
};
[[nodiscard]] uint8 LottieCacheKeyShift(
uint8 replacementsTag,
StickerLottieSize sizeTag);
[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
not_null<Data::DocumentMedia*> media,

View file

@ -15,9 +15,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_file_origin.h"
#include "data/stickers/data_stickers_set.h"
#include "lottie/lottie_common.h"
#include "lottie/lottie_single_player.h"
#include "lottie/lottie_emoji.h"
#include "chat_helpers/stickers_lottie.h"
#include "ui/text/text_block.h"
#include "ui/ui_utility.h"
#include "apiwrap.h"
namespace Data {
@ -29,6 +30,25 @@ struct CustomEmojiId {
namespace {
using SizeTag = CustomEmojiManager::SizeTag;
[[nodiscard]] ChatHelpers::StickerLottieSize LottieSizeFromTag(SizeTag tag) {
using LottieSize = ChatHelpers::StickerLottieSize;
switch (tag) {
case SizeTag::Normal: return LottieSize::MessageHistory;
case SizeTag::Large: return LottieSize::EmojiInteraction;
}
Unexpected("SizeTag value in CustomEmojiManager-LottieSizeFromTag.");
}
[[nodiscard]] int SizeFromTag(SizeTag tag) {
switch (tag) {
case SizeTag::Normal: return Ui::Emoji::GetSizeNormal();
case SizeTag::Large: return Ui::Emoji::GetSizeLarge();
}
Unexpected("SizeTag value in CustomEmojiManager-SizeFromTag.");
}
[[nodiscard]] QString SerializeCustomEmojiId(const CustomEmojiId &id) {
return QString::number(id.id)
+ '@'
@ -37,6 +57,15 @@ namespace {
+ QString::number(id.set.accessHash);
}
[[nodiscard]] QString SerializeCustomEmojiId(
not_null<DocumentData*> document) {
const auto sticker = document->sticker();
return SerializeCustomEmojiId({
sticker ? sticker->set : StickerSetIdentifier(),
document->id,
});
}
[[nodiscard]] CustomEmojiId ParseCustomEmojiData(QStringView data) {
const auto parts = data.split('@');
if (parts.size() != 2) {
@ -56,120 +85,83 @@ namespace {
};
}
class CustomEmojiWithData {
public:
explicit CustomEmojiWithData(const QString &data);
QString entityData();
private:
const QString _data;
};
CustomEmojiWithData::CustomEmojiWithData(const QString &data) : _data(data) {
}
QString CustomEmojiWithData::entityData() {
return _data;
}
class DocumentCustomEmoji final : public CustomEmojiWithData {
public:
DocumentCustomEmoji(
const QString &data,
not_null<DocumentData*> document,
Fn<void()> update);
void paint(QPainter &p, int x, int y, const QColor &preview);
private:
not_null<DocumentData*> _document;
std::shared_ptr<Data::DocumentMedia> _media;
std::unique_ptr<Lottie::SinglePlayer> _lottie;
Fn<void()> _update;
rpl::lifetime _lifetime;
};
DocumentCustomEmoji::DocumentCustomEmoji(
const QString &data,
not_null<DocumentData*> document,
Fn<void()> update)
: CustomEmojiWithData(data)
, _document(document)
, _update(update) {
}
void DocumentCustomEmoji::paint(QPainter &p, int x, int y, const QColor &preview) {
if (!_media) {
_media = _document->createMediaView();
_media->automaticLoad(_document->stickerSetOrigin(), nullptr);
}
if (_media->loaded() && !_lottie) {
const auto size = Ui::Emoji::GetSizeNormal();
_lottie = ChatHelpers::LottiePlayerFromDocument(
_media.get(),
nullptr,
ChatHelpers::StickerLottieSize::MessageHistory,
QSize(size, size),
Lottie::Quality::High);
_lottie->updates() | rpl::start_with_next(_update, _lifetime);
}
if (_lottie && _lottie->ready()) {
const auto frame = _lottie->frame();
p.drawImage(
QRect(
x,
y,
frame.width() / frame.devicePixelRatio(),
frame.height() / frame.devicePixelRatio()),
frame);
_lottie->markFrameShown();
}
}
} // namespace
class CustomEmojiLoader final
: public Ui::CustomEmoji::Loader
, public base::has_weak_ptr {
public:
CustomEmojiLoader(not_null<Session*> owner, const CustomEmojiId id);
CustomEmojiLoader(
not_null<Session*> owner,
const CustomEmojiId id,
SizeTag tag);
CustomEmojiLoader(not_null<DocumentData*> document, SizeTag tag);
[[nodiscard]] bool resolving() const;
void resolved(not_null<DocumentData*> document);
void load(Fn<void(Ui::CustomEmoji::Caching)> ready) override;
QString entityData() override;
void load(Fn<void(LoadResult)> loaded) override;
bool loading() override;
void cancel() override;
Ui::CustomEmoji::Preview preview() override;
private:
struct Resolve {
Fn<void(Ui::CustomEmoji::Caching)> requested;
Fn<void(LoadResult)> requested;
QString entityData;
};
struct Process {
std::shared_ptr<DocumentMedia> media;
Fn<void(Ui::CustomEmoji::Caching)> callback;
Fn<void(LoadResult)> loaded;
base::has_weak_ptr guard;
rpl::lifetime lifetime;
};
struct Load {
struct Requested {
not_null<DocumentData*> document;
std::unique_ptr<Process> process;
};
struct Lookup : Requested {
};
struct Load : Requested {
};
[[nodiscard]] static std::variant<Resolve, Load> InitialState(
void check();
[[nodiscard]] Storage::Cache::Key cacheKey(
not_null<DocumentData*> document) const;
void startCacheLookup(
not_null<Lookup*> lookup,
Fn<void(LoadResult)> loaded);
void lookupDone(
not_null<Lookup*> lookup,
std::optional<Ui::CustomEmoji::Cache> result);
void loadNoCache(
not_null<DocumentData*> document,
Fn<void(LoadResult)> loaded);
[[nodiscard]] static std::variant<Resolve, Lookup, Load> InitialState(
not_null<Session*> owner,
const CustomEmojiId &id);
std::variant<Resolve, Load> _state;
std::variant<Resolve, Lookup, Load> _state;
SizeTag _tag = SizeTag::Normal;
};
CustomEmojiLoader::CustomEmojiLoader(
not_null<Session*> owner,
const CustomEmojiId id)
: _state(InitialState(owner, id)) {
const CustomEmojiId id,
SizeTag tag)
: _state(InitialState(owner, id))
, _tag(tag) {
}
CustomEmojiLoader::CustomEmojiLoader(
not_null<DocumentData*> document,
SizeTag tag)
: _state(Lookup{ document })
, _tag(tag) {
}
bool CustomEmojiLoader::resolving() const {
@ -180,41 +172,179 @@ void CustomEmojiLoader::resolved(not_null<DocumentData*> document) {
Expects(resolving());
auto requested = std::move(v::get<Resolve>(_state).requested);
_state = Load{ document };
_state = Lookup{ document };
if (requested) {
load(std::move(requested));
}
}
void CustomEmojiLoader::load(Fn<void(Ui::CustomEmoji::Caching)> ready) {
void CustomEmojiLoader::load(Fn<void(LoadResult)> loaded) {
if (const auto resolve = std::get_if<Resolve>(&_state)) {
resolve->requested = std::move(ready);
resolve->requested = std::move(loaded);
} else if (const auto lookup = std::get_if<Lookup>(&_state)) {
if (!lookup->process) {
startCacheLookup(lookup, std::move(loaded));
} else {
lookup->process->loaded = std::move(loaded);
}
} else if (const auto load = std::get_if<Load>(&_state)) {
if (!load->process) {
load->process = std::make_unique<Process>(Process{
.media = load->document->createMediaView(),
.callback = std::move(ready),
.loaded = std::move(loaded),
});
load->process->media->checkStickerLarge();
if (load->process->media->loaded()) {
check();
} else {
load->document->session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
check();
}, load->process->lifetime);
}
} else {
load->process->callback = std::move(ready);
load->process->loaded = std::move(loaded);
}
}
}
QString CustomEmojiLoader::entityData() {
if (const auto resolve = std::get_if<Resolve>(&_state)) {
return resolve->entityData;
} else if (const auto lookup = std::get_if<Lookup>(&_state)) {
return SerializeCustomEmojiId(lookup->document);
} else if (const auto load = std::get_if<Load>(&_state)) {
return SerializeCustomEmojiId(load->document);
}
Unexpected("State in CustomEmojiLoader::entityData.");
}
bool CustomEmojiLoader::loading() {
if (const auto resolve = std::get_if<Resolve>(&_state)) {
return (resolve->requested != nullptr);
} else if (const auto lookup = std::get_if<Lookup>(&_state)) {
return (lookup->process != nullptr);
} else if (const auto load = std::get_if<Load>(&_state)) {
return (load->process != nullptr);
}
return false;
}
Storage::Cache::Key CustomEmojiLoader::cacheKey(
not_null<DocumentData*> document) const {
const auto baseKey = document->bigFileBaseCacheKey();
if (!baseKey) {
return {};
}
return Storage::Cache::Key{
baseKey.high,
baseKey.low + ChatHelpers::LottieCacheKeyShift(
0x0F,
LottieSizeFromTag(_tag)),
};
}
void CustomEmojiLoader::startCacheLookup(
not_null<Lookup*> lookup,
Fn<void(LoadResult)> loaded) {
const auto document = lookup->document;
const auto key = cacheKey(document);
if (!key) {
loadNoCache(document, std::move(loaded));
return;
}
lookup->process = std::make_unique<Process>(Process{
.loaded = std::move(loaded),
});
const auto weak = base::make_weak(&lookup->process->guard);
document->owner().cacheBigFile().get(key, [=](QByteArray value) {
auto cache = Ui::CustomEmoji::Cache::FromSerialized(value);
crl::on_main(weak, [=, result = std::move(cache)]() mutable {
lookupDone(lookup, std::move(result));
});
});
}
void CustomEmojiLoader::lookupDone(
not_null<Lookup*> lookup,
std::optional<Ui::CustomEmoji::Cache> result) {
const auto document = lookup->document;
if (!result) {
loadNoCache(document, std::move(lookup->process->loaded));
return;
}
const auto tag = _tag;
auto loader = [=] {
return std::make_unique<CustomEmojiLoader>(document, tag);
};
lookup->process->loaded(Ui::CustomEmoji::Cached(
SerializeCustomEmojiId(document),
std::move(loader),
std::move(*result)));
}
void CustomEmojiLoader::loadNoCache(
not_null<DocumentData*> document,
Fn<void(LoadResult)> loaded) {
_state = Load{ document };
load(std::move(loaded));
}
void CustomEmojiLoader::check() {
using namespace Ui::CustomEmoji;
const auto load = std::get_if<Load>(&_state);
Assert(load != nullptr);
Assert(load->process != nullptr);
const auto media = load->process->media.get();
const auto document = media->owner();
const auto data = media->bytes();
const auto filepath = document->filepath();
if (data.isEmpty() && filepath.isEmpty()) {
return;
}
load->process->lifetime.destroy();
const auto tag = _tag;
const auto size = SizeFromTag(_tag);
auto bytes = Lottie::ReadContent(data, filepath);
auto loader = [=] {
return std::make_unique<CustomEmojiLoader>(document, tag);
};
auto put = [=, key = cacheKey(document)](QByteArray value) {
document->owner().cacheBigFile().put(key, std::move(value));
};
auto generator = [=, bytes = Lottie::ReadContent(data, filepath)]() {
return std::make_unique<Lottie::EmojiGenerator>(bytes);
};
auto renderer = std::make_unique<Renderer>(RendererDescriptor{
.generator = std::move(generator),
.put = std::move(put),
.loader = std::move(loader),
.size = SizeFromTag(_tag),
});
base::take(load->process)->loaded(Caching{
std::move(renderer),
SerializeCustomEmojiId(document),
});
}
auto CustomEmojiLoader::InitialState(
not_null<Session*> owner,
const CustomEmojiId &id)
-> std::variant<Resolve, Load> {
-> std::variant<Resolve, Lookup, Load> {
const auto document = owner->document(id.id);
if (!document->isNull()) {
return Load{ document };
return Lookup{ document };
}
return Resolve();
}
void CustomEmojiLoader::cancel() {
if (const auto load = std::get_if<Load>(&_state)) {
if (const auto lookup = std::get_if<Lookup>(&_state)) {
base::take(lookup->process);
} else if (const auto load = std::get_if<Load>(&_state)) {
if (base::take(load->process)) {
load->document->cancel();
}
@ -222,22 +352,28 @@ void CustomEmojiLoader::cancel() {
}
Ui::CustomEmoji::Preview CustomEmojiLoader::preview() {
if (const auto load = std::get_if<Load>(&_state)) {
if (const auto process = load->process.get()) {
const auto dimensions = load->document->dimensions;
if (!dimensions.width()) {
return {};
}
const auto scale = (Ui::Emoji::GetSizeNormal() * 1.)
/ (style::DevicePixelRatio() * dimensions.width());
return { process->media->thumbnailPath(), scale };
using Preview = Ui::CustomEmoji::Preview;
const auto make = [&](not_null<DocumentData*> document) -> Preview {
const auto dimensions = document->dimensions;
if (!document->inlineThumbnailIsPath()
|| !dimensions.width()) {
return {};
}
const auto scale = (SizeFromTag(_tag) * 1.)
/ (style::DevicePixelRatio() * dimensions.width());
return { document->createMediaView()->thumbnailPath(), scale };
};
if (const auto lookup = std::get_if<Lookup>(&_state)) {
return make(lookup->document);
} else if (const auto load = std::get_if<Load>(&_state)) {
return make(load->document);
}
return {};
}
CustomEmojiManager::CustomEmojiManager(not_null<Session*> owner)
: _owner(owner) {
: _owner(owner)
, _repaintTimer([=] { invokeRepaints(); }) {
}
CustomEmojiManager::~CustomEmojiManager() = default;
@ -256,20 +392,30 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(
auto j = i->second.find(parsed.id);
if (j == end(i->second)) {
using Loading = Ui::CustomEmoji::Loading;
auto loader = std::make_unique<CustomEmojiLoader>(_owner, parsed);
auto loader = std::make_unique<CustomEmojiLoader>(
_owner,
parsed,
SizeTag::Normal);
if (loader->resolving()) {
_loaders[parsed.id].push_back(base::make_weak(loader.get()));
}
const auto repaint = [=](
not_null<Ui::CustomEmoji::Instance*> instance,
Ui::CustomEmoji::RepaintRequest request) {
repaintLater(instance, request);
};
j = i->second.emplace(
parsed.id,
std::make_unique<Ui::CustomEmoji::Instance>(data, Loading{
std::make_unique<Ui::CustomEmoji::Instance>(Loading{
std::move(loader),
Ui::CustomEmoji::Preview()
})).first;
}, std::move(repaint))).first;
}
requestSetIfNeeded(parsed);
return std::make_unique<Ui::CustomEmoji::Object>(j->second.get());
return std::make_unique<Ui::CustomEmoji::Object>(
j->second.get(),
std::move(update));
}
void CustomEmojiManager::requestSetIfNeeded(const CustomEmojiId &id) {
@ -322,6 +468,64 @@ void CustomEmojiManager::requestSetIfNeeded(const CustomEmojiId &id) {
}).send();
}
void CustomEmojiManager::repaintLater(
not_null<Ui::CustomEmoji::Instance*> instance,
Ui::CustomEmoji::RepaintRequest request) {
auto &bunch = _repaints[request.duration];
if (bunch.when < request.when) {
bunch.when = request.when;
}
bunch.instances.emplace_back(instance);
scheduleRepaintTimer();
}
void CustomEmojiManager::scheduleRepaintTimer() {
if (_repaintTimerScheduled) {
return;
}
_repaintTimerScheduled = true;
Ui::PostponeCall(this, [=] {
_repaintTimerScheduled = false;
auto next = crl::time();
for (const auto &[duration, bunch] : _repaints) {
if (!next || next > bunch.when) {
next = bunch.when;
}
}
if (next && (!_repaintNext || _repaintNext > next)) {
const auto now = crl::now();
if (now >= next) {
_repaintNext = 0;
_repaintTimer.cancel();
invokeRepaints();
} else {
_repaintNext = next;
_repaintTimer.callOnce(next - now);
}
}
});
}
void CustomEmojiManager::invokeRepaints() {
_repaintNext = 0;
const auto now = crl::now();
for (auto i = begin(_repaints); i != end(_repaints);) {
if (i->second.when > now) {
++i;
continue;
}
auto bunch = std::move(i->second);
i = _repaints.erase(i);
for (const auto &weak : bunch.instances) {
if (const auto strong = weak.get()) {
strong->repaint();
}
}
}
scheduleRepaintTimer();
}
Main::Session &CustomEmojiManager::session() const {
return _owner->session();
}

View file

@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "ui/text/custom_emoji_instance.h"
#include "base/timer.h"
#include "base/weak_ptr.h"
struct StickerSetIdentifier;
@ -21,8 +23,13 @@ class Session;
struct CustomEmojiId;
class CustomEmojiLoader;
class CustomEmojiManager final {
class CustomEmojiManager final : public base::has_weak_ptr {
public:
enum class SizeTag {
Normal,
Large,
};
CustomEmojiManager(not_null<Session*> owner);
~CustomEmojiManager();
@ -40,8 +47,17 @@ private:
base::flat_set<uint64> documents;
base::flat_set<uint64> waiting;
};
struct RepaintBunch {
crl::time when = 0;
std::vector<base::weak_ptr<Ui::CustomEmoji::Instance>> instances;
};
void requestSetIfNeeded(const CustomEmojiId &id);
void repaintLater(
not_null<Ui::CustomEmoji::Instance*> instance,
Ui::CustomEmoji::RepaintRequest request);
void scheduleRepaintTimer();
void invokeRepaints();
const not_null<Session*> _owner;
@ -53,6 +69,11 @@ private:
uint64,
std::vector<base::weak_ptr<CustomEmojiLoader>>> _loaders;
base::flat_map<crl::time, RepaintBunch> _repaints;
crl::time _repaintNext = 0;
base::Timer _repaintTimer;
bool _repaintTimerScheduled = false;
};
void FillTestCustomEmoji(

View file

@ -275,8 +275,12 @@ Message::Message(
}
Message::~Message() {
if (_comments) {
if (_comments || _heavyCustomEmoji) {
_comments = nullptr;
if (_heavyCustomEmoji) {
_heavyCustomEmoji = false;
message()->_text.unloadCustomEmoji();
}
checkHeavyPart();
}
}
@ -1241,8 +1245,20 @@ void Message::paintText(
const auto stm = context.messageStyle();
p.setPen(stm->historyTextFg);
p.setFont(st::msgFont);
item->_text.draw(p, trect.x(), trect.y(), trect.width(), style::al_left, 0, -1, context.selection);
if (!_heavyCustomEmoji && item->_text.hasCustomEmoji()) {
const auto custom = item->_text.hasCustomEmoji();
if (custom) {
p.setInactive(delegate()->elementIsGifPaused());
}
item->_text.draw(
p,
trect.x(),
trect.y(),
trect.width(),
style::al_left,
0,
-1,
context.selection);
if (!_heavyCustomEmoji && custom) {
_heavyCustomEmoji = true;
history()->owner().registerHeavyViewPart(const_cast<Message*>(this));
}

View file

@ -7,9 +7,25 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/text/custom_emoji_instance.h"
#include "ui/effects/frame_generator.h"
#include <crl/crl_async.h>
class QPainter;
namespace Ui::CustomEmoji {
namespace {
constexpr auto kMaxFrameDuration = 86400 * crl::time(1000);
struct CacheHelper {
int version = 0;
int size = 0;
int frames = 0;
int length = 0;
};
} // namespace
Preview::Preview(QPainterPath path, float64 scale)
: _data(ScaledPath{ std::move(path), scale }) {
@ -26,6 +42,10 @@ void Preview::paint(QPainter &p, int x, int y, const QColor &preview) {
}
}
bool Preview::image() const {
return v::is<QImage>(_data);
}
void Preview::paintPath(
QPainter &p,
int x,
@ -52,7 +72,15 @@ void Preview::paintPath(
}
}
Cache::Cache(QSize size) : _size(size) {
Cache::Cache(int size) : _size(size) {
}
std::optional<Cache> Cache::FromSerialized(const QByteArray &serialized) {
return {};
}
QByteArray Cache::serialize() {
return {};
}
int Cache::frames() const {
@ -60,53 +88,247 @@ int Cache::frames() const {
}
QImage Cache::frame(int index) const {
return QImage();
Expects(index < _frames);
const auto row = index / kPerRow;
const auto inrow = index % kPerRow;
const auto bytes = _bytes[row].data() + inrow * frameByteSize();
const auto data = reinterpret_cast<const uchar*>(bytes);
return QImage(data, _size, _size, QImage::Format_ARGB32_Premultiplied);
}
int Cache::size() const {
return _size;
}
Preview Cache::makePreview() const {
Expects(_frames > 0);
auto image = frame(0);
image.detach();
return { std::move(image) };
}
void Cache::reserve(int frames) {
const auto rows = (frames + kPerRow - 1) / kPerRow;
if (const auto add = rows - int(_bytes.size()); add > 0) {
_bytes.resize(rows);
for (auto e = end(_bytes), i = e - add; i != e; ++i) {
i->resize(kPerRow * frameByteSize());
}
}
_durations.reserve(frames);
}
Cached::Cached(std::unique_ptr<Unloader> unloader, Cache cache)
: _unloader(std::move(unloader))
, _cache(cache) {
int Cache::frameRowByteSize() const {
return _size * 4;
}
void Cached::paint(QPainter &p, int x, int y) {
p.drawImage(x, y, _cache.frame(0));
int Cache::frameByteSize() const {
return _size * frameRowByteSize();
}
Loading Cached::unload() {
return Loading(_unloader->unload(), Preview(_cache.frame(0)));
void Cache::add(crl::time duration, const QImage &frame) {
Expects(duration < kMaxFrameDuration);
Expects(frame.size() == QSize(_size, _size));
Expects(frame.format() == QImage::Format_ARGB32_Premultiplied);
const auto rowSize = frameRowByteSize();
const auto frameSize = frameByteSize();
const auto row = (_frames / kPerRow);
const auto inrow = (_frames % kPerRow);
const auto rows = row + 1;
while (_bytes.size() < rows) {
_bytes.emplace_back();
_bytes.back().resize(kPerRow * frameSize);
}
const auto perLine = frame.bytesPerLine();
auto dst = _bytes[row].data() + inrow * frameSize;
auto src = frame.constBits();
for (auto y = 0; y != _size; ++y) {
memcpy(dst, src, rowSize);
dst += rowSize;
src += perLine;
}
++_frames;
_durations.push_back(duration);
}
void Cacher::reserve(int frames) {
_cache.reserve(frames);
}
void Cacher::add(crl::time duration, QImage frame) {
}
Cache Cacher::takeCache() {
return std::move(_cache);
}
Caching::Caching(std::unique_ptr<Cacher> cacher, Preview preview)
: _cacher(std::move(cacher))
, _preview(std::move(preview)) {
}
void Caching::paint(QPainter &p, int x, int y, const QColor &preview) {
if (!_cacher->paint(p, x, y)) {
_preview.paint(p, x, y, preview);
void Cache::finish() {
_finished = true;
if (_frame == _frames) {
_frame = 0;
}
}
std::optional<Cached> Caching::ready() {
return _cacher->ready();
PaintFrameResult Cache::paintCurrentFrame(
QPainter &p,
int x,
int y,
crl::time now) {
if (!_frames) {
return {};
}
const auto finishes = now ? currentFrameFinishes() : 0;
if (finishes && now >= finishes) {
++_frame;
if (_finished && _frame == _frames) {
_frame = 0;
}
_shown = now;
} else if (!_shown) {
_shown = now;
}
p.drawImage(
QRect(x, y, _size, _size),
frame(std::min(_frame, _frames - 1)));
const auto next = currentFrameFinishes();
const auto duration = next ? (next - _shown) : 0;
return {
.painted = true,
.next = currentFrameFinishes(),
.duration = duration,
};
}
Loading Caching::cancel() {
return Loading(_cacher->cancel(), std::move(_preview));
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, int x, int y, crl::time now) {
return _cache.paintCurrentFrame(p, x, y, now);
}
Loading Cached::unload() {
return Loading(_unloader(), _cache.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) * style::DevicePixelRatio(),
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));
});
});
}
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(count);
}
}
const auto explicitRepaint = (_cache.frames() == _cache.currentFrame());
_cache.add(duration, frame);
const auto size = _cache.size();
const auto guard = base::make_weak(this);
crl::async([
=,
frame = std::move(frame),
generator = std::move(generator)
]() mutable {
auto rendered = generator->renderNext(
std::move(frame),
QSize(size, size) * style::DevicePixelRatio(),
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));
});
});
if (explicitRepaint && _repaint) {
_repaint();
}
}
void Renderer::finish() {
_finished = true;
_cache.finish();
if (_put) {
_put(_cache.serialize());
}
}
PaintFrameResult Renderer::paint(QPainter &p, int x, int y, crl::time now) {
return _cache.paintCurrentFrame(p, x, y, now);
}
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();
}
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)
@ -114,8 +336,24 @@ Loading::Loading(std::unique_ptr<Loader> loader, Preview preview)
, _preview(std::move(preview)) {
}
void Loading::load(Fn<void(Caching)> done) {
_loader->load(crl::guard(this, std::move(done)));
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, int x, int y, const QColor &preview) {
@ -132,52 +370,97 @@ void Loading::cancel() {
invalidate_weak_ptrs(this);
}
Instance::Instance(const QString &entityData, Loading loading)
Instance::Instance(
Loading loading,
Fn<void(not_null<Instance*>, RepaintRequest)> repaintLater)
: _state(std::move(loading))
, _entityData(entityData) {
, _repaintLater(std::move(repaintLater)) {
}
QString Instance::entityData() const {
return _entityData;
if (const auto loading = std::get_if<Loading>(&_state)) {
return loading->entityData();
} else if (const auto caching = std::get_if<Caching>(&_state)) {
return caching->entityData;
} else if (const auto cached = std::get_if<Cached>(&_state)) {
return cached->entityData();
}
Unexpected("State in Instance::entityData.");
}
void Instance::paint(QPainter &p, int x, int y, const QColor &preview) {
void Instance::paint(
QPainter &p,
int x,
int y,
crl::time now,
const QColor &preview,
bool paused) {
if (const auto loading = std::get_if<Loading>(&_state)) {
loading->paint(p, x, y, preview);
loading->load([=](Caching caching) {
_state = std::move(caching);
loading->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);
} else {
Unexpected("Value in Loader::LoadResult.");
}
});
} else if (const auto caching = std::get_if<Caching>(&_state)) {
caching->paint(p, x, y, preview);
if (auto cached = caching->ready()) {
auto result = caching->renderer->paint(p, x, y, paused ? 0 : now);
if (!result.painted) {
caching->preview.paint(p, x, y, preview);
} else {
if (!caching->preview.image()) {
caching->preview = caching->renderer->makePreview();
}
if (result.next > now) {
_repaintLater(this, { result.next, result.duration });
}
}
if (auto cached = caching->renderer->ready(caching->entityData)) {
_state = std::move(*cached);
}
} else if (const auto cached = std::get_if<Cached>(&_state)) {
cached->paint(p, x, y);
const auto result = cached->paint(p, x, y, paused ? 0 : now);
if (result.next > now) {
_repaintLater(this, { result.next, result.duration });
}
}
}
void Instance::incrementUsage() {
++_usage;
void Instance::repaint() {
for (const auto &object : _usage) {
object->repaint();
}
}
void Instance::decrementUsage() {
Expects(_usage > 0);
void Instance::incrementUsage(not_null<Object*> object) {
_usage.emplace(object);
}
if (--_usage > 0) {
void Instance::decrementUsage(not_null<Object*> object) {
_usage.remove(object);
if (!_usage.empty()) {
return;
}
if (const auto loading = std::get_if<Loading>(&_state)) {
loading->cancel();
} else if (const auto caching = std::get_if<Caching>(&_state)) {
_state = caching->cancel();
_state = Loading{
caching->renderer->cancel(),
std::move(caching->preview),
};
} else if (const auto cached = std::get_if<Cached>(&_state)) {
_state = cached->unload();
}
_repaintLater(this, RepaintRequest());
}
Object::Object(not_null<Instance*> instance)
: _instance(instance) {
Object::Object(not_null<Instance*> instance, Fn<void()> repaint)
: _instance(instance)
, _repaint(std::move(repaint)) {
}
Object::~Object() {
@ -188,19 +471,29 @@ QString Object::entityData() {
return _instance->entityData();
}
void Object::paint(QPainter &p, int x, int y, const QColor &preview) {
void Object::paint(
QPainter &p,
int x,
int y,
crl::time now,
const QColor &preview,
bool paused) {
if (!_using) {
_using = true;
_instance->incrementUsage();
_instance->incrementUsage(this);
}
_instance->paint(p, x, y, preview);
_instance->paint(p, x, y, now, preview, paused);
}
void Object::unload() {
if (_using) {
_using = false;
_instance->decrementUsage();
_instance->decrementUsage(this);
}
}
void Object::repaint() {
_repaint();
}
} // namespace Ui::CustomEmoji

View file

@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class QColor;
class QPainter;
namespace Ui {
class FrameGenerator;
} // namespace Ui
namespace Ui::CustomEmoji {
class Preview final {
@ -23,6 +27,7 @@ public:
Preview(QPainterPath path, float64 scale);
void paint(QPainter &p, int x, int y, const QColor &preview);
[[nodiscard]] bool image() const;
[[nodiscard]] explicit operator bool() const {
return !v::is_null(_data);
@ -45,81 +50,124 @@ private:
};
struct PaintFrameResult {
bool painted = false;
crl::time next = 0;
crl::time duration = 0;
};
class Cache final {
public:
Cache(QSize size);
Cache(int size);
[[nodiscard]] static std::optional<Cache> FromSerialized(
const QByteArray &serialized);
[[nodiscard]] QByteArray serialize();
[[nodiscard]] int size() const;
[[nodiscard]] int frames() const;
[[nodiscard]] QImage 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,
int x,
int y,
crl::time now);
[[nodiscard]] int currentFrame() const;
private:
static constexpr auto kPerRow = 30;
static constexpr auto kPerRow = 16;
[[nodiscard]] int frameRowByteSize() const;
[[nodiscard]] int frameByteSize() const;
[[nodiscard]] crl::time currentFrameFinishes() const;
std::vector<bytes::vector> _bytes;
std::vector<int> _durations;
QSize _size;
crl::time _shown = 0;
int _frame = 0;
int _size = 0;
int _frames = 0;
bool _finished = false;
};
class Loader;
class Loading;
class Unloader {
public:
[[nodiscard]] virtual std::unique_ptr<Loader> unload() = 0;
virtual ~Unloader() = default;
};
class Cached final {
public:
Cached(std::unique_ptr<Unloader> unloader, Cache cache);
Cached(
const QString &entityData,
Fn<std::unique_ptr<Loader>()> unloader,
Cache cache);
void paint(QPainter &p, int x, int y);
[[nodiscard]] QString entityData() const;
PaintFrameResult paint(QPainter &p, int x, int y, crl::time now);
[[nodiscard]] Loading unload();
private:
std::unique_ptr<Unloader> _unloader;
Fn<std::unique_ptr<Loader>()> _unloader;
Cache _cache;
QString _entityData;
};
class Cacher {
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:
virtual bool paint(QPainter &p, int x, int y) = 0;
[[nodiscard]] virtual std::optional<Cached> ready() = 0;
[[nodiscard]] virtual std::unique_ptr<Loader> cancel() = 0;
virtual ~Cacher() = default;
explicit Renderer(RendererDescriptor &&descriptor);
virtual ~Renderer() = default;
protected:
void reserve(int frames);
void add(crl::time duration, QImage frame);
PaintFrameResult paint(QPainter &p, int x, int y, crl::time now);
[[nodiscard]] std::optional<Cached> ready(const QString &entityData);
[[nodiscard]] std::unique_ptr<Loader> cancel();
[[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 finish();
Cache _cache;
std::unique_ptr<Ui::FrameGenerator> _generator;
Fn<void(QByteArray)> _put;
Fn<void()> _repaint;
Fn<std::unique_ptr<Loader>()> _loader;
bool _finished = false;
};
class Caching final {
public:
Caching(std::unique_ptr<Cacher> cacher, Preview preview);
void paint(QPainter &p, int x, int y, const QColor &preview);
[[nodiscard]] std::optional<Cached> ready();
[[nodiscard]] Loading cancel();
private:
std::unique_ptr<Cacher> _cacher;
Preview _preview;
struct Caching {
std::unique_ptr<Renderer> renderer;
QString entityData;
Preview preview;
};
class Loader {
public:
virtual void load(Fn<void(Caching)> ready) = 0;
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;
@ -129,7 +177,10 @@ class Loading final : public base::has_weak_ptr {
public:
Loading(std::unique_ptr<Loader> loader, Preview preview);
void load(Fn<void(Caching)> done);
[[nodiscard]] QString entityData() const;
void load(Fn<void(Loader::LoadResult)> done);
[[nodiscard]] bool loading() const;
void paint(QPainter &p, int x, int y, const QColor &preview);
void cancel();
@ -139,21 +190,36 @@ private:
};
class Instance final {
struct RepaintRequest {
crl::time when = 0;
crl::time duration = 0;
};
class Object;
class Instance final : public base::has_weak_ptr {
public:
Instance(const QString &entityData, Loading loading);
Instance(
Loading loading,
Fn<void(not_null<Instance*>, RepaintRequest)> repaintLater);
[[nodiscard]] QString entityData() const;
void paint(QPainter &p, int x, int y, const QColor &preview);
void paint(
QPainter &p,
int x,
int y,
crl::time now,
const QColor &preview,
bool paused);
void incrementUsage();
void decrementUsage();
void incrementUsage(not_null<Object*> object);
void decrementUsage(not_null<Object*> object);
void repaint();
private:
std::variant<Loading, Caching, Cached> _state;
QString _entityData;
int _usage = 0;
base::flat_set<not_null<Object*>> _usage;
Fn<void(not_null<Instance*> that, RepaintRequest)> _repaintLater;
};
@ -165,15 +231,24 @@ public:
class Object final : public Ui::Text::CustomEmoji {
public:
Object(not_null<Instance*> instance);
Object(not_null<Instance*> instance, Fn<void()> repaint);
~Object();
QString entityData() override;
void paint(QPainter &p, int x, int y, const QColor &preview) override;
void paint(
QPainter &p,
int x,
int y,
crl::time now,
const QColor &preview,
bool paused) override;
void unload() override;
void repaint();
private:
const not_null<Instance*> _instance;
Fn<void()> _repaint;
bool _using = false;
};

View file

@ -841,6 +841,7 @@ void SessionController::setupPremiumToast() {
}) | rpl::distinct_until_changed() | rpl::skip(
1
) | rpl::filter([=](bool premium) {
session().mtp().requestConfig();
return premium;
}) | rpl::start_with_next([=] {
Ui::Toast::Show(

@ -1 +1 @@
Subproject commit 7ae2b9480ef61e222fd46e7806e46875d523502c
Subproject commit 301708287d619c0ba01058d151d46aba2a15ec64

@ -1 +1 @@
Subproject commit 187110f43820a70d20b77e4063286b692d53294d
Subproject commit 6ef5ec3410ef5db33cb6413a7f05cc87a2c970bd