Start optimized custom emoji, show path thumbnail.

This commit is contained in:
John Preston 2022-06-24 15:14:23 +04:00
parent 2e6733e433
commit 9d280da80b
7 changed files with 619 additions and 50 deletions

View File

@ -13,27 +13,31 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_document.h"
#include "data/data_document_media.h"
#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 "chat_helpers/stickers_lottie.h"
#include "ui/text/text_block.h"
#include "apiwrap.h"
namespace Data {
namespace {
struct CustomEmojiId {
StickerSetIdentifier set;
uint64 id = 0;
};
namespace {
[[nodiscard]] QString SerializeCustomEmojiId(const CustomEmojiId &id) {
const auto &info = id.set;
const auto set = info.id
? (QString::number(info.id) + ':' + QString::number(info.accessHash))
: info.shortName;
return QString::number(id.id) + '@' + set;
return QString::number(id.id)
+ '@'
+ QString::number(id.set.id)
+ ':'
+ QString::number(id.set.accessHash);
}
[[nodiscard]] CustomEmojiId ParseCustomEmojiData(const QString &data) {
[[nodiscard]] CustomEmojiId ParseCustomEmojiData(QStringView data) {
const auto parts = data.split('@');
if (parts.size() != 2) {
return {};
@ -43,23 +47,20 @@ struct CustomEmojiId {
return {};
}
const auto second = parts[1].split(':');
if (const auto set = second[0].toULongLong()) {
return {
.set = {.id = set, .accessHash = second[1].toULongLong() },
.id = id
};
}
return {
.set = {.shortName = second[1] },
.set = {
.id = second[0].toULongLong(),
.accessHash = second[1].toULongLong(),
},
.id = id
};
}
class CustomEmojiWithData : public Ui::Text::CustomEmoji {
class CustomEmojiWithData {
public:
explicit CustomEmojiWithData(const QString &data);
QString entityData() final override;
QString entityData();
private:
const QString _data;
@ -80,7 +81,7 @@ public:
not_null<DocumentData*> document,
Fn<void()> update);
void paint(QPainter &p, int x, int y) override;
void paint(QPainter &p, int x, int y, const QColor &preview);
private:
not_null<DocumentData*> _document;
@ -100,7 +101,7 @@ DocumentCustomEmoji::DocumentCustomEmoji(
, _update(update) {
}
void DocumentCustomEmoji::paint(QPainter &p, int x, int y) {
void DocumentCustomEmoji::paint(QPainter &p, int x, int y, const QColor &preview) {
if (!_media) {
_media = _document->createMediaView();
_media->automaticLoad(_document->stickerSetOrigin(), nullptr);
@ -128,32 +129,112 @@ void DocumentCustomEmoji::paint(QPainter &p, int x, int y) {
}
}
class ResolvingCustomEmoji final : public CustomEmojiWithData {
public:
ResolvingCustomEmoji(const QString &data, Fn<void()> update);
} // namespace
void paint(QPainter &p, int x, int y) override;
class CustomEmojiLoader final
: public Ui::CustomEmoji::Loader
, public base::has_weak_ptr {
public:
CustomEmojiLoader(not_null<Session*> owner, const CustomEmojiId id);
[[nodiscard]] bool resolving() const;
void resolved(not_null<DocumentData*> document);
void load(Fn<void(Ui::CustomEmoji::Caching)> ready) override;
void cancel() override;
Ui::CustomEmoji::Preview preview() override;
private:
std::optional<DocumentCustomEmoji> _resolved;
Fn<void()> _update;
struct Resolve {
Fn<void(Ui::CustomEmoji::Caching)> requested;
};
struct Process {
std::shared_ptr<DocumentMedia> media;
Fn<void(Ui::CustomEmoji::Caching)> callback;
rpl::lifetime lifetime;
};
struct Load {
not_null<DocumentData*> document;
std::unique_ptr<Process> process;
};
[[nodiscard]] static std::variant<Resolve, Load> InitialState(
not_null<Session*> owner,
const CustomEmojiId &id);
std::variant<Resolve, Load> _state;
};
ResolvingCustomEmoji::ResolvingCustomEmoji(
const QString &data,
Fn<void()> update)
: CustomEmojiWithData(data)
, _update(update) {
CustomEmojiLoader::CustomEmojiLoader(
not_null<Session*> owner,
const CustomEmojiId id)
: _state(InitialState(owner, id)) {
}
void ResolvingCustomEmoji::paint(QPainter &p, int x, int y) {
if (_resolved) {
_resolved->paint(p, x, y);
bool CustomEmojiLoader::resolving() const {
return v::is<Resolve>(_state);
}
void CustomEmojiLoader::resolved(not_null<DocumentData*> document) {
Expects(resolving());
auto requested = std::move(v::get<Resolve>(_state).requested);
_state = Load{ document };
if (requested) {
load(std::move(requested));
}
}
} // namespace
void CustomEmojiLoader::load(Fn<void(Ui::CustomEmoji::Caching)> ready) {
if (const auto resolve = std::get_if<Resolve>(&_state)) {
resolve->requested = std::move(ready);
} 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),
});
load->process->media->checkStickerLarge();
} else {
load->process->callback = std::move(ready);
}
}
}
auto CustomEmojiLoader::InitialState(
not_null<Session*> owner,
const CustomEmojiId &id)
-> std::variant<Resolve, Load> {
const auto document = owner->document(id.id);
if (!document->isNull()) {
return Load{ document };
}
return Resolve();
}
void CustomEmojiLoader::cancel() {
if (const auto load = std::get_if<Load>(&_state)) {
if (base::take(load->process)) {
load->document->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 };
}
}
return {};
}
CustomEmojiManager::CustomEmojiManager(not_null<Session*> owner)
: _owner(owner) {
@ -165,14 +246,80 @@ std::unique_ptr<Ui::Text::CustomEmoji> CustomEmojiManager::create(
const QString &data,
Fn<void()> update) {
const auto parsed = ParseCustomEmojiData(data);
if (!parsed.id) {
if (!parsed.id || !parsed.set.id) {
return nullptr;
}
const auto document = _owner->document(parsed.id);
if (!document->isNull()) {
return std::make_unique<DocumentCustomEmoji>(data, document, update);
auto i = _instances.find(parsed.set.id);
if (i == end(_instances)) {
i = _instances.emplace(parsed.set.id).first;
}
return std::make_unique<ResolvingCustomEmoji>(data, update);
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);
if (loader->resolving()) {
_loaders[parsed.id].push_back(base::make_weak(loader.get()));
}
j = i->second.emplace(
parsed.id,
std::make_unique<Ui::CustomEmoji::Instance>(data, Loading{
std::move(loader),
Ui::CustomEmoji::Preview()
})).first;
}
requestSetIfNeeded(parsed);
return std::make_unique<Ui::CustomEmoji::Object>(j->second.get());
}
void CustomEmojiManager::requestSetIfNeeded(const CustomEmojiId &id) {
const auto setId = id.set.id;
auto i = _sets.find(setId);
if (i == end(_sets)) {
i = _sets.emplace(setId).first;
}
if (i->second.documents.contains(id.id)) {
return;
} else if (!i->second.waiting.emplace(id.id).second
|| i->second.requestId) {
return;
}
const auto api = &_owner->session().api();
i->second.requestId = api->request(MTPmessages_GetStickerSet(
InputStickerSet(id.set),
MTP_int(i->second.hash)
)).done([=](const MTPmessages_StickerSet &result) {
const auto i = _sets.find(setId);
Assert(i != end(_sets));
i->second.requestId = 0;
result.match([&](const MTPDmessages_stickerSet &data) {
data.vset().match([&](const MTPDstickerSet &data) {
i->second.hash = data.vhash().v;
});
for (const auto &entry : data.vdocuments().v) {
const auto document = _owner->processDocument(entry);
const auto id = document->id;
i->second.documents.emplace(id);
i->second.waiting.remove(id);
if (const auto loaders = _loaders.take(id)) {
for (const auto &weak : *loaders) {
if (const auto strong = weak.get()) {
strong->resolved(document);
}
}
}
}
}, [&](const MTPDmessages_stickerSetNotModified &) {
});
for (const auto &id : base::take(i->second.waiting)) {
DEBUG_LOG(("API Error: Sticker '%1' not found for emoji.").arg(id));
}
}).fail([=] {
const auto i = _sets.find(setId);
Assert(i != end(_sets));
i->second.requestId = 0;
LOG(("API Error: Failed getting set '%1' for emoji.").arg(setId));
}).send();
}
Main::Session &CustomEmojiManager::session() const {
@ -192,17 +339,18 @@ void FillTestCustomEmoji(
auto length = 0;
if (const auto emoji = Ui::Emoji::Find(ch, end, &length)) {
if (const auto found = pack->stickerForEmoji(emoji)) {
Expects(found.document->sticker() != nullptr);
text.entities.push_back({
EntityType::CustomEmoji,
(ch - begin),
length,
SerializeCustomEmojiId({
found.document->sticker()->set,
found.document->id,
}),
});
Assert(found.document->sticker() != nullptr);
if (found.document->sticker()->set.id) {
text.entities.push_back({
EntityType::CustomEmoji,
(ch - begin),
length,
SerializeCustomEmojiId({
found.document->sticker()->set,
found.document->id,
}),
});
}
}
ch += length;
} else if (ch->isHighSurrogate()

View File

@ -7,6 +7,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/text/custom_emoji_instance.h"
struct StickerSetIdentifier;
namespace Main {
class Session;
} // namespace Main
@ -14,6 +18,8 @@ class Session;
namespace Data {
class Session;
struct CustomEmojiId;
class CustomEmojiLoader;
class CustomEmojiManager final {
public:
@ -28,8 +34,25 @@ public:
[[nodiscard]] Session &owner() const;
private:
struct Set {
int32 hash = 0;
mtpRequestId requestId = 0;
base::flat_set<uint64> documents;
base::flat_set<uint64> waiting;
};
void requestSetIfNeeded(const CustomEmojiId &id);
const not_null<Session*> _owner;
base::flat_map<uint64, base::flat_map<
uint64,
std::unique_ptr<Ui::CustomEmoji::Instance>>> _instances;
base::flat_map<uint64, Set> _sets;
base::flat_map<
uint64,
std::vector<base::weak_ptr<CustomEmojiLoader>>> _loaders;
};
void FillTestCustomEmoji(

View File

@ -1242,6 +1242,10 @@ void Message::paintText(
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()) {
_heavyCustomEmoji = true;
history()->owner().registerHeavyViewPart(const_cast<Message*>(this));
}
}
PointState Message::pointState(QPoint point) const {
@ -1368,12 +1372,16 @@ void Message::toggleCommentsButtonRipple(bool pressed) {
}
bool Message::hasHeavyPart() const {
return _comments || Element::hasHeavyPart();
return _heavyCustomEmoji || _comments || Element::hasHeavyPart();
}
void Message::unloadHeavyPart() {
Element::unloadHeavyPart();
_comments = nullptr;
if (_heavyCustomEmoji) {
_heavyCustomEmoji = false;
message()->_text.unloadCustomEmoji();
}
}
bool Message::showForwardsFromSender(

View File

@ -252,6 +252,7 @@ private:
Ui::Text::String _rightBadge;
int _bubbleWidthLimit = 0;
mutable bool _heavyCustomEmoji = false;
BottomInfo _bottomInfo;

View File

@ -0,0 +1,206 @@
/*
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"
class QPainter;
namespace Ui::CustomEmoji {
Preview::Preview(QPainterPath path, float64 scale)
: _data(ScaledPath{ std::move(path), scale }) {
}
Preview::Preview(QImage image) : _data(std::move(image)) {
}
void Preview::paint(QPainter &p, int x, int y, const QColor &preview) {
if (const auto path = std::get_if<ScaledPath>(&_data)) {
paintPath(p, x, y, preview, *path);
} else if (const auto image = std::get_if<QImage>(&_data)) {
p.drawImage(x, y, *image);
}
}
void Preview::paintPath(
QPainter &p,
int x,
int y,
const QColor &preview,
const ScaledPath &path) {
auto hq = PainterHighQualityEnabler(p);
p.setBrush(preview);
p.setPen(Qt::NoPen);
const auto scale = path.scale;
const auto required = (scale != 1.);
if (required) {
p.save();
}
p.translate(x, y);
if (required) {
p.scale(scale, scale);
}
p.drawPath(path.path);
if (required) {
p.restore();
} else {
p.translate(-x, -y);
}
}
Cache::Cache(QSize size) : _size(size) {
}
int Cache::frames() const {
return _frames;
}
QImage Cache::frame(int index) const {
return QImage();
}
void Cache::reserve(int frames) {
}
Cached::Cached(std::unique_ptr<Unloader> unloader, Cache cache)
: _unloader(std::move(unloader))
, _cache(cache) {
}
void Cached::paint(QPainter &p, int x, int y) {
p.drawImage(x, y, _cache.frame(0));
}
Loading Cached::unload() {
return Loading(_unloader->unload(), Preview(_cache.frame(0)));
}
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);
}
}
std::optional<Cached> Caching::ready() {
return _cacher->ready();
}
Loading Caching::cancel() {
return Loading(_cacher->cancel(), std::move(_preview));
}
Loading::Loading(std::unique_ptr<Loader> loader, Preview preview)
: _loader(std::move(loader))
, _preview(std::move(preview)) {
}
void Loading::load(Fn<void(Caching)> done) {
_loader->load(crl::guard(this, std::move(done)));
}
void Loading::paint(QPainter &p, int x, int y, const QColor &preview) {
if (!_preview) {
if (auto preview = _loader->preview()) {
_preview = std::move(preview);
}
}
_preview.paint(p, x, y, preview);
}
void Loading::cancel() {
_loader->cancel();
invalidate_weak_ptrs(this);
}
Instance::Instance(const QString &entityData, Loading loading)
: _state(std::move(loading))
, _entityData(entityData) {
}
QString Instance::entityData() const {
return _entityData;
}
void Instance::paint(QPainter &p, int x, int y, const QColor &preview) {
if (const auto loading = std::get_if<Loading>(&_state)) {
loading->paint(p, x, y, preview);
loading->load([=](Caching caching) {
_state = std::move(caching);
});
} else if (const auto caching = std::get_if<Caching>(&_state)) {
caching->paint(p, x, y, preview);
if (auto cached = caching->ready()) {
_state = std::move(*cached);
}
} else if (const auto cached = std::get_if<Cached>(&_state)) {
cached->paint(p, x, y);
}
}
void Instance::incrementUsage() {
++_usage;
}
void Instance::decrementUsage() {
Expects(_usage > 0);
if (--_usage > 0) {
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();
} else if (const auto cached = std::get_if<Cached>(&_state)) {
_state = cached->unload();
}
}
Object::Object(not_null<Instance*> instance)
: _instance(instance) {
}
Object::~Object() {
unload();
}
QString Object::entityData() {
return _instance->entityData();
}
void Object::paint(QPainter &p, int x, int y, const QColor &preview) {
if (!_using) {
_using = true;
_instance->incrementUsage();
}
_instance->paint(p, x, y, preview);
}
void Object::unload() {
if (_using) {
_using = false;
_instance->decrementUsage();
}
}
} // namespace Ui::CustomEmoji

View File

@ -0,0 +1,181 @@
/*
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_block.h"
#include "base/weak_ptr.h"
#include "base/bytes.h"
class QColor;
class QPainter;
namespace Ui::CustomEmoji {
class Preview final {
public:
Preview() = default;
Preview(QImage image);
Preview(QPainterPath path, float64 scale);
void paint(QPainter &p, int x, int y, const QColor &preview);
[[nodiscard]] explicit operator bool() const {
return !v::is_null(_data);
}
private:
struct ScaledPath {
QPainterPath path;
float64 scale = 1.;
};
void paintPath(
QPainter &p,
int x,
int y,
const QColor &preview,
const ScaledPath &path);
std::variant<v::null_t, ScaledPath, QImage> _data;
};
class Cache final {
public:
Cache(QSize size);
[[nodiscard]] int frames() const;
[[nodiscard]] QImage frame(int index) const;
void reserve(int frames);
private:
static constexpr auto kPerRow = 30;
std::vector<bytes::vector> _bytes;
std::vector<int> _durations;
QSize _size;
int _frames = 0;
};
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);
void paint(QPainter &p, int x, int y);
[[nodiscard]] Loading unload();
private:
std::unique_ptr<Unloader> _unloader;
Cache _cache;
};
class Cacher {
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;
protected:
void reserve(int frames);
void add(crl::time duration, QImage frame);
[[nodiscard]] Cache takeCache();
private:
Cache _cache;
};
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;
};
class Loader {
public:
virtual void load(Fn<void(Caching)> ready) = 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);
void load(Fn<void(Caching)> done);
void paint(QPainter &p, int x, int y, const QColor &preview);
void cancel();
private:
std::unique_ptr<Loader> _loader;
Preview _preview;
};
class Instance final {
public:
Instance(const QString &entityData, Loading loading);
[[nodiscard]] QString entityData() const;
void paint(QPainter &p, int x, int y, const QColor &preview);
void incrementUsage();
void decrementUsage();
private:
std::variant<Loading, Caching, Cached> _state;
QString _entityData;
int _usage = 0;
};
class Delegate {
public:
[[nodiscard]] virtual bool paused() = 0;
virtual ~Delegate() = default;
};
class Object final : public Ui::Text::CustomEmoji {
public:
Object(not_null<Instance*> instance);
~Object();
QString entityData() override;
void paint(QPainter &p, int x, int y, const QColor &preview) override;
void unload() override;
private:
const not_null<Instance*> _instance;
bool _using = false;
};
} // namespace Ui::CustomEmoji

View File

@ -232,6 +232,8 @@ PRIVATE
ui/effects/round_checkbox.h
ui/effects/scroll_content_shadow.cpp
ui/effects/scroll_content_shadow.h
ui/text/custom_emoji_instance.cpp
ui/text/custom_emoji_instance.h
ui/text/format_song_name.cpp
ui/text/format_song_name.h
ui/text/format_values.cpp