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

View file

@ -49,7 +49,7 @@ using StickersSetFlags = base::flags<StickersSetFlag>;
namespace ChatHelpers { namespace ChatHelpers {
enum class StickerLottieSize : uchar { enum class StickerLottieSize : uint8 {
MessageHistory, MessageHistory,
StickerSet, StickerSet,
StickersPanel, StickersPanel,
@ -66,6 +66,9 @@ enum class StickerLottieSize : uchar {
EmojiInteractionReserved7, EmojiInteractionReserved7,
PremiumReactionPreview, PremiumReactionPreview,
}; };
[[nodiscard]] uint8 LottieCacheKeyShift(
uint8 replacementsTag,
StickerLottieSize sizeTag);
[[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument( [[nodiscard]] std::unique_ptr<Lottie::SinglePlayer> LottiePlayerFromDocument(
not_null<Data::DocumentMedia*> media, 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/data_file_origin.h"
#include "data/stickers/data_stickers_set.h" #include "data/stickers/data_stickers_set.h"
#include "lottie/lottie_common.h" #include "lottie/lottie_common.h"
#include "lottie/lottie_single_player.h" #include "lottie/lottie_emoji.h"
#include "chat_helpers/stickers_lottie.h" #include "chat_helpers/stickers_lottie.h"
#include "ui/text/text_block.h" #include "ui/text/text_block.h"
#include "ui/ui_utility.h"
#include "apiwrap.h" #include "apiwrap.h"
namespace Data { namespace Data {
@ -29,6 +30,25 @@ struct CustomEmojiId {
namespace { 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) { [[nodiscard]] QString SerializeCustomEmojiId(const CustomEmojiId &id) {
return QString::number(id.id) return QString::number(id.id)
+ '@' + '@'
@ -37,6 +57,15 @@ namespace {
+ QString::number(id.set.accessHash); + 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) { [[nodiscard]] CustomEmojiId ParseCustomEmojiData(QStringView data) {
const auto parts = data.split('@'); const auto parts = data.split('@');
if (parts.size() != 2) { 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 } // namespace
class CustomEmojiLoader final class CustomEmojiLoader final
: public Ui::CustomEmoji::Loader : public Ui::CustomEmoji::Loader
, public base::has_weak_ptr { , public base::has_weak_ptr {
public: 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; [[nodiscard]] bool resolving() const;
void resolved(not_null<DocumentData*> document); 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; void cancel() override;
Ui::CustomEmoji::Preview preview() override; Ui::CustomEmoji::Preview preview() override;
private: private:
struct Resolve { struct Resolve {
Fn<void(Ui::CustomEmoji::Caching)> requested; Fn<void(LoadResult)> requested;
QString entityData;
}; };
struct Process { struct Process {
std::shared_ptr<DocumentMedia> media; std::shared_ptr<DocumentMedia> media;
Fn<void(Ui::CustomEmoji::Caching)> callback; Fn<void(LoadResult)> loaded;
base::has_weak_ptr guard;
rpl::lifetime lifetime; rpl::lifetime lifetime;
}; };
struct Load { struct Requested {
not_null<DocumentData*> document; not_null<DocumentData*> document;
std::unique_ptr<Process> process; 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, not_null<Session*> owner,
const CustomEmojiId &id); const CustomEmojiId &id);
std::variant<Resolve, Load> _state; std::variant<Resolve, Lookup, Load> _state;
SizeTag _tag = SizeTag::Normal;
}; };
CustomEmojiLoader::CustomEmojiLoader( CustomEmojiLoader::CustomEmojiLoader(
not_null<Session*> owner, not_null<Session*> owner,
const CustomEmojiId id) const CustomEmojiId id,
: _state(InitialState(owner, 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 { bool CustomEmojiLoader::resolving() const {
@ -180,41 +172,179 @@ void CustomEmojiLoader::resolved(not_null<DocumentData*> document) {
Expects(resolving()); Expects(resolving());
auto requested = std::move(v::get<Resolve>(_state).requested); auto requested = std::move(v::get<Resolve>(_state).requested);
_state = Load{ document }; _state = Lookup{ document };
if (requested) { if (requested) {
load(std::move(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)) { 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)) { } else if (const auto load = std::get_if<Load>(&_state)) {
if (!load->process) { if (!load->process) {
load->process = std::make_unique<Process>(Process{ load->process = std::make_unique<Process>(Process{
.media = load->document->createMediaView(), .media = load->document->createMediaView(),
.callback = std::move(ready), .loaded = std::move(loaded),
}); });
load->process->media->checkStickerLarge(); load->process->media->checkStickerLarge();
if (load->process->media->loaded()) {
check();
} else {
load->document->session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
check();
}, load->process->lifetime);
}
} else { } 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( auto CustomEmojiLoader::InitialState(
not_null<Session*> owner, not_null<Session*> owner,
const CustomEmojiId &id) const CustomEmojiId &id)
-> std::variant<Resolve, Load> { -> std::variant<Resolve, Lookup, Load> {
const auto document = owner->document(id.id); const auto document = owner->document(id.id);
if (!document->isNull()) { if (!document->isNull()) {
return Load{ document }; return Lookup{ document };
} }
return Resolve(); return Resolve();
} }
void CustomEmojiLoader::cancel() { 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)) { if (base::take(load->process)) {
load->document->cancel(); load->document->cancel();
} }
@ -222,22 +352,28 @@ void CustomEmojiLoader::cancel() {
} }
Ui::CustomEmoji::Preview CustomEmojiLoader::preview() { Ui::CustomEmoji::Preview CustomEmojiLoader::preview() {
if (const auto load = std::get_if<Load>(&_state)) { using Preview = Ui::CustomEmoji::Preview;
if (const auto process = load->process.get()) { const auto make = [&](not_null<DocumentData*> document) -> Preview {
const auto dimensions = load->document->dimensions; const auto dimensions = document->dimensions;
if (!dimensions.width()) { if (!document->inlineThumbnailIsPath()
return {}; || !dimensions.width()) {
} return {};
const auto scale = (Ui::Emoji::GetSizeNormal() * 1.)
/ (style::DevicePixelRatio() * dimensions.width());
return { process->media->thumbnailPath(), scale };
} }
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 {}; return {};
} }
CustomEmojiManager::CustomEmojiManager(not_null<Session*> owner) CustomEmojiManager::CustomEmojiManager(not_null<Session*> owner)
: _owner(owner) { : _owner(owner)
, _repaintTimer([=] { invokeRepaints(); }) {
} }
CustomEmojiManager::~CustomEmojiManager() = default; CustomEmojiManager::~CustomEmojiManager() = default;
@ -256,20 +392,30 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(
auto j = i->second.find(parsed.id); auto j = i->second.find(parsed.id);
if (j == end(i->second)) { if (j == end(i->second)) {
using Loading = Ui::CustomEmoji::Loading; 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()) { if (loader->resolving()) {
_loaders[parsed.id].push_back(base::make_weak(loader.get())); _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( j = i->second.emplace(
parsed.id, parsed.id,
std::make_unique<Ui::CustomEmoji::Instance>(data, Loading{ std::make_unique<Ui::CustomEmoji::Instance>(Loading{
std::move(loader), std::move(loader),
Ui::CustomEmoji::Preview() Ui::CustomEmoji::Preview()
})).first; }, std::move(repaint))).first;
} }
requestSetIfNeeded(parsed); 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) { void CustomEmojiManager::requestSetIfNeeded(const CustomEmojiId &id) {
@ -322,6 +468,64 @@ void CustomEmojiManager::requestSetIfNeeded(const CustomEmojiId &id) {
}).send(); }).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 { Main::Session &CustomEmojiManager::session() const {
return _owner->session(); return _owner->session();
} }

View file

@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once #pragma once
#include "ui/text/custom_emoji_instance.h" #include "ui/text/custom_emoji_instance.h"
#include "base/timer.h"
#include "base/weak_ptr.h"
struct StickerSetIdentifier; struct StickerSetIdentifier;
@ -21,8 +23,13 @@ class Session;
struct CustomEmojiId; struct CustomEmojiId;
class CustomEmojiLoader; class CustomEmojiLoader;
class CustomEmojiManager final { class CustomEmojiManager final : public base::has_weak_ptr {
public: public:
enum class SizeTag {
Normal,
Large,
};
CustomEmojiManager(not_null<Session*> owner); CustomEmojiManager(not_null<Session*> owner);
~CustomEmojiManager(); ~CustomEmojiManager();
@ -40,8 +47,17 @@ private:
base::flat_set<uint64> documents; base::flat_set<uint64> documents;
base::flat_set<uint64> waiting; 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 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; const not_null<Session*> _owner;
@ -53,6 +69,11 @@ private:
uint64, uint64,
std::vector<base::weak_ptr<CustomEmojiLoader>>> _loaders; 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( void FillTestCustomEmoji(

View file

@ -275,8 +275,12 @@ Message::Message(
} }
Message::~Message() { Message::~Message() {
if (_comments) { if (_comments || _heavyCustomEmoji) {
_comments = nullptr; _comments = nullptr;
if (_heavyCustomEmoji) {
_heavyCustomEmoji = false;
message()->_text.unloadCustomEmoji();
}
checkHeavyPart(); checkHeavyPart();
} }
} }
@ -1241,8 +1245,20 @@ void Message::paintText(
const auto stm = context.messageStyle(); const auto stm = context.messageStyle();
p.setPen(stm->historyTextFg); p.setPen(stm->historyTextFg);
p.setFont(st::msgFont); p.setFont(st::msgFont);
item->_text.draw(p, trect.x(), trect.y(), trect.width(), style::al_left, 0, -1, context.selection); const auto custom = item->_text.hasCustomEmoji();
if (!_heavyCustomEmoji && 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; _heavyCustomEmoji = true;
history()->owner().registerHeavyViewPart(const_cast<Message*>(this)); 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/text/custom_emoji_instance.h"
#include "ui/effects/frame_generator.h"
#include <crl/crl_async.h>
class QPainter; class QPainter;
namespace Ui::CustomEmoji { 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) Preview::Preview(QPainterPath path, float64 scale)
: _data(ScaledPath{ std::move(path), 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( void Preview::paintPath(
QPainter &p, QPainter &p,
int x, 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 { int Cache::frames() const {
@ -60,53 +88,247 @@ int Cache::frames() const {
} }
QImage Cache::frame(int index) 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) { 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) int Cache::frameRowByteSize() const {
: _unloader(std::move(unloader)) return _size * 4;
, _cache(cache) {
} }
void Cached::paint(QPainter &p, int x, int y) { int Cache::frameByteSize() const {
p.drawImage(x, y, _cache.frame(0)); return _size * frameRowByteSize();
} }
Loading Cached::unload() { void Cache::add(crl::time duration, const QImage &frame) {
return Loading(_unloader->unload(), Preview(_cache.frame(0))); 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) { void Cache::finish() {
_cache.reserve(frames); _finished = true;
} if (_frame == _frames) {
_frame = 0;
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);
} }
} }
std::optional<Cached> Caching::ready() { PaintFrameResult Cache::paintCurrentFrame(
return _cacher->ready(); 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() { int Cache::currentFrame() const {
return Loading(_cacher->cancel(), std::move(_preview)); 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) 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)) { , _preview(std::move(preview)) {
} }
void Loading::load(Fn<void(Caching)> done) { QString Loading::entityData() const {
_loader->load(crl::guard(this, std::move(done))); 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) { void Loading::paint(QPainter &p, int x, int y, const QColor &preview) {
@ -132,52 +370,97 @@ void Loading::cancel() {
invalidate_weak_ptrs(this); 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)) : _state(std::move(loading))
, _entityData(entityData) { , _repaintLater(std::move(repaintLater)) {
} }
QString Instance::entityData() const { 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)) { if (const auto loading = std::get_if<Loading>(&_state)) {
loading->paint(p, x, y, preview); loading->paint(p, x, y, preview);
loading->load([=](Caching caching) { loading->load([=](Loader::LoadResult result) {
_state = std::move(caching); 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)) { } else if (const auto caching = std::get_if<Caching>(&_state)) {
caching->paint(p, x, y, preview); auto result = caching->renderer->paint(p, x, y, paused ? 0 : now);
if (auto cached = caching->ready()) { 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); _state = std::move(*cached);
} }
} else if (const auto cached = std::get_if<Cached>(&_state)) { } 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() { void Instance::repaint() {
++_usage; for (const auto &object : _usage) {
object->repaint();
}
} }
void Instance::decrementUsage() { void Instance::incrementUsage(not_null<Object*> object) {
Expects(_usage > 0); _usage.emplace(object);
}
if (--_usage > 0) { void Instance::decrementUsage(not_null<Object*> object) {
_usage.remove(object);
if (!_usage.empty()) {
return; return;
} }
if (const auto loading = std::get_if<Loading>(&_state)) { if (const auto loading = std::get_if<Loading>(&_state)) {
loading->cancel(); loading->cancel();
} else if (const auto caching = std::get_if<Caching>(&_state)) { } 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)) { } else if (const auto cached = std::get_if<Cached>(&_state)) {
_state = cached->unload(); _state = cached->unload();
} }
_repaintLater(this, RepaintRequest());
} }
Object::Object(not_null<Instance*> instance) Object::Object(not_null<Instance*> instance, Fn<void()> repaint)
: _instance(instance) { : _instance(instance)
, _repaint(std::move(repaint)) {
} }
Object::~Object() { Object::~Object() {
@ -188,19 +471,29 @@ QString Object::entityData() {
return _instance->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) { if (!_using) {
_using = true; _using = true;
_instance->incrementUsage(); _instance->incrementUsage(this);
} }
_instance->paint(p, x, y, preview); _instance->paint(p, x, y, now, preview, paused);
} }
void Object::unload() { void Object::unload() {
if (_using) { if (_using) {
_using = false; _using = false;
_instance->decrementUsage(); _instance->decrementUsage(this);
} }
} }
void Object::repaint() {
_repaint();
}
} // namespace Ui::CustomEmoji } // namespace Ui::CustomEmoji

View file

@ -14,6 +14,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class QColor; class QColor;
class QPainter; class QPainter;
namespace Ui {
class FrameGenerator;
} // namespace Ui
namespace Ui::CustomEmoji { namespace Ui::CustomEmoji {
class Preview final { class Preview final {
@ -23,6 +27,7 @@ public:
Preview(QPainterPath path, float64 scale); Preview(QPainterPath path, float64 scale);
void paint(QPainter &p, int x, int y, const QColor &preview); void paint(QPainter &p, int x, int y, const QColor &preview);
[[nodiscard]] bool image() const;
[[nodiscard]] explicit operator bool() const { [[nodiscard]] explicit operator bool() const {
return !v::is_null(_data); 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 { class Cache final {
public: 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]] int frames() const;
[[nodiscard]] QImage frame(int index) const; [[nodiscard]] QImage frame(int index) const;
void reserve(int frames); 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: 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<bytes::vector> _bytes;
std::vector<int> _durations; std::vector<int> _durations;
QSize _size; crl::time _shown = 0;
int _frame = 0;
int _size = 0;
int _frames = 0; int _frames = 0;
bool _finished = false;
}; };
class Loader; class Loader;
class Loading; class Loading;
class Unloader {
public:
[[nodiscard]] virtual std::unique_ptr<Loader> unload() = 0;
virtual ~Unloader() = default;
};
class Cached final { class Cached final {
public: 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(); [[nodiscard]] Loading unload();
private: private:
std::unique_ptr<Unloader> _unloader; Fn<std::unique_ptr<Loader>()> _unloader;
Cache _cache; 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: public:
virtual bool paint(QPainter &p, int x, int y) = 0; explicit Renderer(RendererDescriptor &&descriptor);
[[nodiscard]] virtual std::optional<Cached> ready() = 0; virtual ~Renderer() = default;
[[nodiscard]] virtual std::unique_ptr<Loader> cancel() = 0;
virtual ~Cacher() = default;
protected: PaintFrameResult paint(QPainter &p, int x, int y, crl::time now);
void reserve(int frames); [[nodiscard]] std::optional<Cached> ready(const QString &entityData);
void add(crl::time duration, QImage frame); [[nodiscard]] std::unique_ptr<Loader> cancel();
[[nodiscard]] Preview makePreview() const;
void setRepaintCallback(Fn<void()> repaint);
[[nodiscard]] Cache takeCache(); [[nodiscard]] Cache takeCache();
private: private:
void frameReady(
std::unique_ptr<Ui::FrameGenerator> generator,
crl::time duration,
QImage frame);
void finish();
Cache _cache; 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 { struct Caching {
public: std::unique_ptr<Renderer> renderer;
Caching(std::unique_ptr<Cacher> cacher, Preview preview); QString entityData;
void paint(QPainter &p, int x, int y, const QColor &preview); Preview preview;
[[nodiscard]] std::optional<Cached> ready();
[[nodiscard]] Loading cancel();
private:
std::unique_ptr<Cacher> _cacher;
Preview _preview;
}; };
class Loader { class Loader {
public: 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; virtual void cancel() = 0;
[[nodiscard]] virtual Preview preview() = 0; [[nodiscard]] virtual Preview preview() = 0;
virtual ~Loader() = default; virtual ~Loader() = default;
@ -129,7 +177,10 @@ class Loading final : public base::has_weak_ptr {
public: public:
Loading(std::unique_ptr<Loader> loader, Preview preview); 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 paint(QPainter &p, int x, int y, const QColor &preview);
void cancel(); 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: public:
Instance(const QString &entityData, Loading loading); Instance(
Loading loading,
Fn<void(not_null<Instance*>, RepaintRequest)> repaintLater);
[[nodiscard]] QString entityData() const; [[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 incrementUsage(not_null<Object*> object);
void decrementUsage(); void decrementUsage(not_null<Object*> object);
void repaint();
private: private:
std::variant<Loading, Caching, Cached> _state; std::variant<Loading, Caching, Cached> _state;
QString _entityData; base::flat_set<not_null<Object*>> _usage;
Fn<void(not_null<Instance*> that, RepaintRequest)> _repaintLater;
int _usage = 0;
}; };
@ -165,15 +231,24 @@ public:
class Object final : public Ui::Text::CustomEmoji { class Object final : public Ui::Text::CustomEmoji {
public: public:
Object(not_null<Instance*> instance); Object(not_null<Instance*> instance, Fn<void()> repaint);
~Object(); ~Object();
QString entityData() override; 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 unload() override;
void repaint();
private: private:
const not_null<Instance*> _instance; const not_null<Instance*> _instance;
Fn<void()> _repaint;
bool _using = false; bool _using = false;
}; };

View file

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

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

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