Use FrameGenerator-based AnimatedIcon for reactions.

This commit is contained in:
John Preston 2022-08-29 16:34:21 +04:00
parent d9a6d5f508
commit ed3f246510
24 changed files with 225 additions and 1181 deletions

View file

@ -82,7 +82,6 @@ PRIVATE
desktop-app::lib_webview
desktop-app::lib_ffmpeg
desktop-app::lib_stripe
desktop-app::external_lz4
desktop-app::external_rlottie
desktop-app::external_zlib
desktop-app::external_kcoreaddons

View file

@ -7,7 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "boxes/peers/edit_peer_reactions.h"
#include "boxes/reactions_settings_box.h" // AddReactionLottieIcon
#include "boxes/reactions_settings_box.h" // AddReactionAnimatedIcon
#include "data/data_message_reactions.h"
#include "data/data_peer.h"
#include "data/data_chat.h"
@ -58,7 +58,7 @@ void EditAllowedReactionsBox(
tr::lng_manage_peer_reactions_enable(),
st::manageGroupButton.button);
if (!list.empty()) {
AddReactionLottieIcon(
AddReactionAnimatedIcon(
enabled,
enabled->sizeValue(
) | rpl::map([=](const QSize &size) {
@ -101,7 +101,7 @@ void EditAllowedReactionsBox(
container,
rpl::single(entry.title),
st::manageGroupButton.button);
AddReactionLottieIcon(
AddReactionAnimatedIcon(
button,
button->sizeValue(
) | rpl::map([=](const QSize &size) {

View file

@ -19,7 +19,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_element.h"
#include "history/view/reactions/history_view_reactions_strip.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_icon.h"
#include "boxes/premium_preview_box.h"
#include "main/main_session.h"
#include "settings/settings_common.h"
@ -33,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/widgets/labels.h"
#include "ui/widgets/scroll_area.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/animated_icon.h"
#include "window/section_widget.h"
#include "window/window_session_controller.h"
#include "styles/style_boxes.h"
@ -230,7 +230,7 @@ void AddMessage(
}
const auto index = state->icons.flag ? 1 : 0;
state->icons.lifetimes[index] = rpl::lifetime();
AddReactionLottieIcon(
AddReactionAnimatedIcon(
container,
widget->geometryValue(
) | rpl::map([=](const QRect &r) {
@ -253,7 +253,7 @@ void AddMessage(
} // namespace
void AddReactionLottieIcon(
void AddReactionAnimatedIcon(
not_null<Ui::RpWidget*> parent,
rpl::producer<QPoint> iconPositionValue,
int iconSize,
@ -261,11 +261,10 @@ void AddReactionLottieIcon(
rpl::producer<> &&selects,
rpl::producer<> &&destroys,
not_null<rpl::lifetime*> stateLifetime) {
struct State {
struct Entry {
std::shared_ptr<Data::DocumentMedia> media;
std::shared_ptr<Lottie::Icon> icon;
std::shared_ptr<Ui::AnimatedIcon> icon;
};
Entry appear;
Entry select;
@ -332,7 +331,7 @@ void AddReactionLottieIcon(
p.scale(progress, progress);
}
const auto paintFrame = [&](not_null<Lottie::Icon*> animation) {
const auto paintFrame = [&](not_null<Ui::AnimatedIcon*> animation) {
const auto frame = animation->frame();
p.drawImage(
QRect(
@ -346,7 +345,7 @@ void AddReactionLottieIcon(
const auto appear = state->appear.icon.get();
if (appear && !state->appearAnimated) {
state->appearAnimated = true;
appear->animate(update, 0, appear->framesCount() - 1);
appear->animate(update);
}
if (appear && appear->animating()) {
paintFrame(appear);
@ -360,7 +359,7 @@ void AddReactionLottieIcon(
) | rpl::start_with_next([=] {
const auto select = state->select.icon.get();
if (select && !select->animating()) {
select->animate(update, 0, select->framesCount() - 1);
select->animate(update);
}
}, widget->lifetime());
@ -437,7 +436,7 @@ void ReactionsSettingsBox(
}
const auto iconSize = st::settingsReactionSize;
AddReactionLottieIcon(
AddReactionAnimatedIcon(
button,
button->sizeValue(
) | rpl::map([=, left = button->st().iconLeft](const QSize &s) {

View file

@ -20,7 +20,7 @@ namespace Data {
struct Reaction;
} // namespace Data
void AddReactionLottieIcon(
void AddReactionAnimatedIcon(
not_null<Ui::RpWidget*> parent,
rpl::producer<QPoint> iconPositionValue,
int iconSize,

View file

@ -17,6 +17,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "main/main_session.h"
#include "main/main_session_settings.h"
#include "lottie/lottie_animation.h"
#include "lottie/lottie_frame_generator.h"
#include "ffmpeg/ffmpeg_frame_generator.h"
#include "history/history_item.h"
#include "history/history.h"
#include "window/themes/window_theme_preview.h"
@ -505,4 +507,47 @@ void DocumentMedia::ReadOrGenerateThumbnail(
document->owner().cache().get(document->goodThumbnailCacheKey(), got);
}
auto DocumentIconFrameGenerator(not_null<DocumentMedia*> media)
-> FnMut<std::unique_ptr<Ui::FrameGenerator>()> {
if (!media->loaded()) {
return nullptr;
}
using Type = StickerType;
const auto document = media->owner();
const auto content = media->bytes();
const auto fromFile = content.isEmpty();
const auto type = document->sticker()
? document->sticker()->type
: (document->isVideoFile() || document->isAnimation())
? Type::Webm
: Type::Webp;
const auto &location = media->owner()->location(true);
if (fromFile && !location.accessEnable()) {
return nullptr;
}
return [=]() -> std::unique_ptr<Ui::FrameGenerator> {
const auto bytes = Lottie::ReadContent(content, location.name());
if (fromFile) {
location.accessDisable();
}
if (bytes.isEmpty()) {
return nullptr;
}
switch (type) {
case Type::Tgs:
return std::make_unique<Lottie::FrameGenerator>(bytes);
case Type::Webm:
return std::make_unique<FFmpeg::FrameGenerator>(bytes);
case Type::Webp:
return std::make_unique<Ui::ImageFrameGenerator>(bytes);
}
Unexpected("Document type in DocumentIconFrameGenerator.");
};
}
auto DocumentIconFrameGenerator(const std::shared_ptr<DocumentMedia> &media)
-> FnMut<std::unique_ptr<Ui::FrameGenerator>()> {
return DocumentIconFrameGenerator(media.get());
}
} // namespace Data

View file

@ -12,6 +12,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
class Image;
class FileLoader;
namespace Ui {
class FrameGenerator;
} // namespace Ui
namespace Media {
namespace Clip {
enum class Notification;
@ -112,4 +116,11 @@ private:
};
[[nodiscard]] auto DocumentIconFrameGenerator(not_null<DocumentMedia*> media)
-> FnMut<std::unique_ptr<Ui::FrameGenerator>()>;
[[nodiscard]] auto DocumentIconFrameGenerator(
const std::shared_ptr<DocumentMedia> &media)
-> FnMut<std::unique_ptr<Ui::FrameGenerator>()>;
} // namespace Data

View file

@ -20,9 +20,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_document_media.h"
#include "data/data_peer_values.h"
#include "data/stickers/data_custom_emoji.h"
#include "lottie/lottie_icon.h"
#include "storage/localimageloader.h"
#include "ui/image/image_location_factory.h"
#include "ui/animated_icon.h"
#include "mtproto/mtproto_config.h"
#include "base/timer_rpl.h"
#include "base/call_delayed.h"
@ -300,7 +300,7 @@ void Reactions::preloadImageFor(const ReactionId &id) {
? nullptr
: i->centerIcon
? i->centerIcon
: i->appearAnimation.get();
: i->selectAnimation.get();
if (document) {
loadImage(set, document, !i->centerIcon);
} else if (!_waitingForList) {
@ -337,7 +337,7 @@ QImage Reactions::resolveImageFor(
auto &set = (i != end(_images)) ? i->second : _images[emoji];
const auto resolve = [&](QImage &image, int size) {
const auto factor = style::DevicePixelRatio();
const auto frameSize = set.fromAppearAnimation
const auto frameSize = set.fromSelectAnimation
? (size / 2)
: size;
image = set.icon->frame().scaled(
@ -345,7 +345,7 @@ QImage Reactions::resolveImageFor(
frameSize * factor,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
if (set.fromAppearAnimation) {
if (set.fromSelectAnimation) {
auto result = QImage(
size * factor,
size * factor,
@ -385,7 +385,7 @@ void Reactions::resolveImages() {
? nullptr
: i->centerIcon
? i->centerIcon
: i->appearAnimation.get();
: i->selectAnimation.get();
if (document) {
loadImage(set, document, !i->centerIcon);
} else {
@ -398,16 +398,16 @@ void Reactions::resolveImages() {
void Reactions::loadImage(
ImageSet &set,
not_null<DocumentData*> document,
bool fromAppearAnimation) {
bool fromSelectAnimation) {
if (!set.bottomInfo.isNull() || set.icon) {
return;
} else if (!set.media) {
set.fromAppearAnimation = fromAppearAnimation;
set.fromSelectAnimation = fromSelectAnimation;
set.media = document->createMediaView();
set.media->checkStickerLarge();
}
if (set.media->loaded()) {
setLottie(set);
setAnimatedIcon(set);
} else if (!_imagesLoadLifetime) {
document->session().downloaderTaskFinished(
) | rpl::start_with_next([=] {
@ -416,13 +416,11 @@ void Reactions::loadImage(
}
}
void Reactions::setLottie(ImageSet &set) {
void Reactions::setAnimatedIcon(ImageSet &set) {
const auto size = style::ConvertScale(kSizeForDownscale);
set.icon = Lottie::MakeIcon({
.path = set.media->owner()->filepath(true),
.json = set.media->bytes(),
set.icon = Ui::MakeAnimatedIcon({
.generator = DocumentIconFrameGenerator(set.media),
.sizeOverride = QSize(size, size),
.frame = -1,
});
set.media = nullptr;
}
@ -433,7 +431,7 @@ void Reactions::downloadTaskFinished() {
if (!set.media) {
continue;
} else if (set.media->loaded()) {
setLottie(set);
setAnimatedIcon(set);
} else {
hasOne = true;
}
@ -644,8 +642,6 @@ std::optional<Reaction> Reactions::parse(const MTPAvailableReaction &entry) {
if (!known) {
LOG(("API Error: Unknown emoji in reactions: %1").arg(emoji));
}
const auto selectAnimation = _owner->processDocument(
data.vselect_animation());
return known
? std::make_optional(Reaction{
.id = ReactionId{ emoji },
@ -653,7 +649,8 @@ std::optional<Reaction> Reactions::parse(const MTPAvailableReaction &entry) {
//.staticIcon = _owner->processDocument(data.vstatic_icon()),
.appearAnimation = _owner->processDocument(
data.vappear_animation()),
.selectAnimation = selectAnimation,
.selectAnimation = _owner->processDocument(
data.vselect_animation()),
//.activateAnimation = _owner->processDocument(
// data.vactivate_animation()),
//.activateEffects = _owner->processDocument(

View file

@ -11,14 +11,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_message_reaction_id.h"
#include "data/stickers/data_custom_emoji.h"
namespace Ui {
class AnimatedIcon;
} // namespace Ui
namespace Ui::Text {
class CustomEmoji;
} // namespace Ui::Text
namespace Lottie {
class Icon;
} // namespace Lottie
namespace Data {
class DocumentMedia;
@ -117,8 +117,8 @@ private:
QImage bottomInfo;
QImage inlineList;
std::shared_ptr<DocumentMedia> media;
std::unique_ptr<Lottie::Icon> icon;
bool fromAppearAnimation = false;
std::unique_ptr<Ui::AnimatedIcon> icon;
bool fromSelectAnimation = false;
};
[[nodiscard]] not_null<CustomEmojiManager::Listener*> resolveListener();
@ -148,8 +148,8 @@ private:
void loadImage(
ImageSet &set,
not_null<DocumentData*> document,
bool fromAppearAnimation);
void setLottie(ImageSet &set);
bool fromSelectAnimation);
void setAnimatedIcon(ImageSet &set);
void resolveImages();
void downloadTaskFinished();

View file

@ -17,8 +17,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_message_reactions.h"
#include "data/stickers/data_stickers.h"
#include "lottie/lottie_common.h"
#include "lottie/lottie_emoji.h"
#include "ffmpeg/ffmpeg_emoji.h"
#include "lottie/lottie_frame_generator.h"
#include "ffmpeg/ffmpeg_frame_generator.h"
#include "chat_helpers/stickers_lottie.h"
#include "ui/widgets/input_fields.h"
#include "ui/text/text_custom_emoji.h"
@ -321,9 +321,9 @@ void CustomEmojiLoader::check() {
-> std::unique_ptr<Ui::FrameGenerator> {
switch (type) {
case StickerType::Tgs:
return std::make_unique<Lottie::EmojiGenerator>(bytes);
return std::make_unique<Lottie::FrameGenerator>(bytes);
case StickerType::Webm:
return std::make_unique<FFmpeg::EmojiGenerator>(bytes);
return std::make_unique<FFmpeg::FrameGenerator>(bytes);
case StickerType::Webp:
return std::make_unique<Ui::ImageFrameGenerator>(bytes);
}

View file

@ -5,7 +5,7 @@ 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 "ffmpeg/ffmpeg_emoji.h"
#include "ffmpeg/ffmpeg_frame_generator.h"
#include "ffmpeg/ffmpeg_utility.h"
#include "base/debug_log.h"
@ -17,7 +17,7 @@ constexpr auto kMaxArea = 1920 * 1080 * 4;
} // namespace
class EmojiGenerator::Impl final {
class FrameGenerator::Impl final {
public:
explicit Impl(const QByteArray &bytes);
@ -25,6 +25,11 @@ public:
QImage storage,
QSize size,
Qt::AspectRatioMode mode);
[[nodiscard]] Frame renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode);
void jumpToStart();
private:
struct ReadFrame {
@ -35,10 +40,6 @@ private:
void readNextFrame();
void resolveNextFrameTiming();
[[nodiscard]] Frame renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode);
[[nodiscard]] QString wrapError(int result) const;
@ -76,13 +77,13 @@ private:
};
EmojiGenerator::Impl::Impl(const QByteArray &bytes)
FrameGenerator::Impl::Impl(const QByteArray &bytes)
: _bytes(bytes) {
_format = MakeFormatPointer(
static_cast<void*>(this),
&EmojiGenerator::Impl::Read,
&FrameGenerator::Impl::Read,
nullptr,
&EmojiGenerator::Impl::Seek);
&FrameGenerator::Impl::Seek);
auto error = 0;
if ((error = avformat_find_stream_info(_format.get(), nullptr))) {
@ -105,11 +106,11 @@ EmojiGenerator::Impl::Impl(const QByteArray &bytes)
_codec = MakeCodecPointer({ .stream = info });
}
int EmojiGenerator::Impl::Read(void *opaque, uint8_t *buf, int buf_size) {
int FrameGenerator::Impl::Read(void *opaque, uint8_t *buf, int buf_size) {
return static_cast<Impl*>(opaque)->read(buf, buf_size);
}
int EmojiGenerator::Impl::read(uint8_t *buf, int buf_size) {
int FrameGenerator::Impl::read(uint8_t *buf, int buf_size) {
const auto available = _bytes.size() - _deviceOffset;
if (available <= 0) {
return -1;
@ -120,14 +121,14 @@ int EmojiGenerator::Impl::read(uint8_t *buf, int buf_size) {
return fill;
}
int64_t EmojiGenerator::Impl::Seek(
int64_t FrameGenerator::Impl::Seek(
void *opaque,
int64_t offset,
int whence) {
return static_cast<Impl*>(opaque)->seek(offset, whence);
}
int64_t EmojiGenerator::Impl::seek(int64_t offset, int whence) {
int64_t FrameGenerator::Impl::seek(int64_t offset, int whence) {
if (whence == AVSEEK_SIZE) {
return _bytes.size();
}
@ -146,7 +147,7 @@ int64_t EmojiGenerator::Impl::seek(int64_t offset, int whence) {
return now;
}
EmojiGenerator::Frame EmojiGenerator::Impl::renderCurrent(
FrameGenerator::Frame FrameGenerator::Impl::renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode) {
@ -238,18 +239,18 @@ EmojiGenerator::Frame EmojiGenerator::Impl::renderCurrent(
transform.rotate(_rotation);
storage = storage.transformed(transform);
}
ClearFrameMemory(_current.frame.get());
const auto duration = _next.frame
? (_next.position - _current.position)
: _current.duration;
return {
.image = std::move(storage),
.duration = duration,
.image = std::move(storage),
.last = !_next.frame,
};
}
EmojiGenerator::Frame EmojiGenerator::Impl::renderNext(
FrameGenerator::Frame FrameGenerator::Impl::renderNext(
QImage storage,
QSize size,
Qt::AspectRatioMode mode) {
@ -264,7 +265,27 @@ EmojiGenerator::Frame EmojiGenerator::Impl::renderNext(
return renderCurrent(std::move(storage), size, mode);
}
void EmojiGenerator::Impl::resolveNextFrameTiming() {
void FrameGenerator::Impl::jumpToStart() {
auto result = 0;
if ((result = avformat_seek_file(_format.get(), _streamId, std::numeric_limits<int64_t>::min(), 0, std::numeric_limits<int64_t>::max(), 0)) < 0) {
if ((result = av_seek_frame(_format.get(), _streamId, 0, AVSEEK_FLAG_BYTE)) < 0) {
if ((result = av_seek_frame(_format.get(), _streamId, 0, AVSEEK_FLAG_FRAME)) < 0) {
if ((result = av_seek_frame(_format.get(), _streamId, 0, 0)) < 0) {
char err[AV_ERROR_MAX_STRING_SIZE] = { 0 };
LOG(("Webm Error: Unable to av_seek_frame() to the start, ") + wrapError(result));
return;
}
}
}
}
avcodec_flush_buffers(_codec.get());
_current = ReadFrame();
_next = ReadFrame();
_currentFrameDelay = _nextFrameDelay = 0;
_framePosition = 0;
}
void FrameGenerator::Impl::resolveNextFrameTiming() {
const auto base = _format->streams[_streamId]->time_base;
const auto duration = _next.frame->pkt_duration;
const auto framePts = _next.frame->pts;
@ -287,7 +308,7 @@ void EmojiGenerator::Impl::resolveNextFrameTiming() {
_next.duration = _nextFrameDelay;
}
void EmojiGenerator::Impl::readNextFrame() {
void FrameGenerator::Impl::readNextFrame() {
auto frame = _next.frame ? base::take(_next.frame) : MakeFramePointer();
while (true) {
auto result = avcodec_receive_frame(_codec.get(), frame.get());
@ -347,28 +368,43 @@ void EmojiGenerator::Impl::readNextFrame() {
}
}
QString EmojiGenerator::Impl::wrapError(int result) const {
QString FrameGenerator::Impl::wrapError(int result) const {
auto error = std::array<char, AV_ERROR_MAX_STRING_SIZE>{};
return u"error %1, %2"_q
.arg(result)
.arg(av_make_error_string(error.data(), error.size(), result));
}
EmojiGenerator::EmojiGenerator(const QByteArray &bytes)
FrameGenerator::FrameGenerator(const QByteArray &bytes)
: _impl(std::make_unique<Impl>(bytes)) {
}
EmojiGenerator::~EmojiGenerator() = default;
FrameGenerator::~FrameGenerator() = default;
int EmojiGenerator::count() {
int FrameGenerator::count() {
return 0;
}
EmojiGenerator::Frame EmojiGenerator::renderNext(
double FrameGenerator::rate() {
return 0.;
}
FrameGenerator::Frame FrameGenerator::renderNext(
QImage storage,
QSize size,
Qt::AspectRatioMode mode) {
return _impl->renderNext(std::move(storage), size, mode);
}
FrameGenerator::Frame FrameGenerator::renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode) {
return _impl->renderCurrent(std::move(storage), size, mode);
}
void FrameGenerator::jumpToStart() {
_impl->jumpToStart();
}
} // namespace FFmpeg

View file

@ -14,16 +14,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace FFmpeg {
class EmojiGenerator final : public Ui::FrameGenerator {
class FrameGenerator final : public Ui::FrameGenerator {
public:
explicit EmojiGenerator(const QByteArray &bytes);
~EmojiGenerator();
explicit FrameGenerator(const QByteArray &bytes);
~FrameGenerator();
int count() override;
double rate() override;
Frame renderNext(
QImage storage,
QSize size,
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override;
Frame renderCurrent(
QImage storage,
QSize size,
Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio) override;
void jumpToStart() override;
private:
class Impl;

View file

@ -14,16 +14,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace Ui {
struct ChatPaintContext;
class AnimatedIcon;
} // namespace Ui
namespace Data {
class Reactions;
} // namespace Data
namespace Lottie {
class Icon;
} // namespace Lottie
namespace HistoryView {
namespace Reactions {
class Animation;
@ -31,7 +28,7 @@ class Animation;
struct ReactionAnimationArgs {
::Data::ReactionId id;
std::shared_ptr<Lottie::Icon> flyIcon;
std::shared_ptr<Ui::AnimatedIcon> flyIcon;
QRect flyFrom;
[[nodiscard]] ReactionAnimationArgs translated(QPoint point) const;

View file

@ -9,7 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_element.h"
#include "history/view/history_view_bottom_info.h"
#include "lottie/lottie_icon.h"
#include "ui/animated_icon.h"
#include "data/data_message_reactions.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
@ -36,7 +36,7 @@ Animation::Animation(
return;
}
const auto resolve = [&](
std::unique_ptr<Lottie::Icon> &icon,
std::unique_ptr<Ui::AnimatedIcon> &icon,
DocumentData *document,
int size) {
if (!document) {
@ -46,9 +46,8 @@ Animation::Animation(
if (!media || !media->loaded()) {
return false;
}
icon = Lottie::MakeIcon({
.path = document->filepath(true),
.json = media->bytes(),
icon = Ui::MakeAnimatedIcon({
.generator = DocumentIconFrameGenerator(media),
.sizeOverride = QSize(size, size),
});
return true;
@ -142,8 +141,8 @@ int Animation::computeParabolicTop(
}
void Animation::startAnimations() {
_center->animate([=] { callback(); }, 0, _center->framesCount() - 1);
_effect->animate([=] { callback(); }, 0, _effect->framesCount() - 1);
_center->animate([=] { callback(); });
_effect->animate([=] { callback(); });
}
void Animation::flyCallback() {

View file

@ -9,8 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/effects/animations.h"
namespace Lottie {
class Icon;
namespace Ui {
class AnimatedIcon;
} // namespace Lottie
namespace Data {
@ -47,9 +47,9 @@ private:
const not_null<::Data::Reactions*> _owner;
Fn<void()> _repaint;
std::shared_ptr<Lottie::Icon> _flyIcon;
std::unique_ptr<Lottie::Icon> _center;
std::unique_ptr<Lottie::Icon> _effect;
std::shared_ptr<Ui::AnimatedIcon> _flyIcon;
std::unique_ptr<Ui::AnimatedIcon> _center;
std::unique_ptr<Ui::AnimatedIcon> _effect;
Ui::Animations::Simple _fly;
QRect _flyFrom;
bool _valid = false;

View file

@ -262,8 +262,4 @@ void SetupManagerList(
not_null<Manager*> manager,
rpl::producer<HistoryItem*> items);
[[nodiscard]] std::shared_ptr<Lottie::Icon> DefaultIconFactory(
not_null<Data::DocumentMedia*> media,
int size);
} // namespace HistoryView

View file

@ -11,8 +11,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "lottie/lottie_icon.h"
#include "main/main_session.h"
#include "ui/effects/frame_generator.h"
#include "ui/animated_icon.h"
#include "styles/style_chat.h"
namespace HistoryView::Reactions {
@ -27,21 +28,31 @@ constexpr auto kHoverScale = 1.24;
return style::ConvertScale(kSizeForDownscale);
}
[[nodiscard]] std::shared_ptr<Lottie::Icon> CreateIcon(
[[nodiscard]] std::shared_ptr<Ui::AnimatedIcon> CreateIcon(
not_null<Data::DocumentMedia*> media,
int size,
int frame) {
int size) {
Expects(media->loaded());
return std::make_shared<Lottie::Icon>(Lottie::IconDescriptor{
.path = media->owner()->filepath(true),
.json = media->bytes(),
return std::make_shared<Ui::AnimatedIcon>(Ui::AnimatedIconDescriptor{
.generator = DocumentIconFrameGenerator(media),
.sizeOverride = QSize(size, size),
.frame = frame,
.limitFps = true,
});
}
[[nodiscard]] std::shared_ptr<Ui::AnimatedIcon> CreateIconSnapshot(
not_null<DocumentData*> document,
not_null<Ui::AnimatedIcon*> existing) {
const auto frame = existing->frame();
return frame.isNull()
? CreateIcon(
document->activeMediaView().get(),
existing->width())
: std::make_shared<Ui::AnimatedIcon>(Ui::AnimatedIconDescriptor{
.generator = [=] { return std::make_unique<Ui::ImageFrameGenerator>(frame); },
.sizeOverride = existing->size(),
});
}
} // namespace
Strip::Strip(
@ -157,7 +168,7 @@ void Strip::paintOne(
} else if (icon.added == AddedButton::Expand) {
paintExpandIcon(p, position, target);
} else {
const auto paintFrame = [&](not_null<Lottie::Icon*> animation) {
const auto paintFrame = [&](not_null<Ui::AnimatedIcon*> animation) {
const auto size = int(std::floor(target.width() + 0.01));
const auto frame = animation->frame({ size, size }, _update);
p.drawImage(target, frame.image);
@ -166,7 +177,7 @@ void Strip::paintOne(
const auto appear = icon.appear.get();
if (appear && !icon.appearAnimated && allowAppearStart) {
icon.appearAnimated = true;
appear->animate(_update, 0, appear->framesCount() - 1);
appear->animate(_update);
}
if (appear && appear->animating()) {
paintFrame(appear);
@ -220,15 +231,9 @@ int Strip::fillChosenIconGetIndex(ChosenReaction &chosen) const {
}
const auto &icon = *i;
if (const auto &appear = icon.appear; appear && appear->animating()) {
chosen.icon = CreateIcon(
icon.appearAnimation->activeMediaView().get(),
appear->width(),
appear->frameIndex());
chosen.icon = CreateIconSnapshot(icon.appearAnimation, appear.get());
} else if (const auto &select = icon.select) {
chosen.icon = CreateIcon(
icon.selectAnimation->activeMediaView().get(),
select->width(),
select->frameIndex());
chosen.icon = CreateIconSnapshot(icon.selectAnimation, select.get());
}
return (i - begin(_icons));
}
@ -307,7 +312,7 @@ void Strip::setSelected(int index) const {
const auto select = skipAnimation ? nullptr : icon.select.get();
if (select && !icon.selectAnimated) {
icon.selectAnimated = true;
select->animate(_update, 0, select->framesCount() - 1);
select->animate(_update);
}
}
};
@ -342,13 +347,13 @@ void Strip::clearAppearAnimations(bool mainAppeared) {
}
icon.selectedScale.stop();
if (const auto select = icon.select.get()) {
select->jumpTo(0, nullptr);
select->jumpToStart(nullptr);
}
icon.selectAnimated = false;
}
if (icon.appearAnimated != main) {
if (const auto appear = icon.appear.get()) {
appear->jumpTo(0, nullptr);
appear->jumpToStart(nullptr);
}
icon.appearAnimated = main;
}
@ -358,7 +363,7 @@ void Strip::clearAppearAnimations(bool mainAppeared) {
void Strip::clearStateForHidden(ReactionIcons &icon) {
if (const auto appear = icon.appear.get()) {
appear->jumpTo(0, nullptr);
appear->jumpToStart(nullptr);
}
if (icon.selected) {
setSelected(-1);
@ -366,7 +371,7 @@ void Strip::clearStateForHidden(ReactionIcons &icon) {
icon.appearAnimated = false;
icon.selectAnimated = false;
if (const auto select = icon.select.get()) {
select->jumpTo(0, nullptr);
select->jumpToStart(nullptr);
}
icon.selectedScale.stop();
}
@ -424,8 +429,8 @@ void Strip::loadIcons() {
}
}
}
if (all && !_icons.empty() && _icons.front().appearAnimation) {
auto &data = _icons.front().appearAnimation->owner().reactions();
if (all && !_icons.empty() && _icons.front().selectAnimation) {
auto &data = _icons.front().selectAnimation->owner().reactions();
for (const auto &icon : _icons) {
data.preloadAnimationsFor(icon.id);
}
@ -562,10 +567,10 @@ IconFactory CachedIconFactory::createMethod() {
};
}
std::shared_ptr<Lottie::Icon> DefaultIconFactory(
std::shared_ptr<Ui::AnimatedIcon> DefaultIconFactory(
not_null<Data::DocumentMedia*> media,
int size) {
return CreateIcon(media, size, 0);
return CreateIcon(media, size);
}
} // namespace HistoryView::Reactions

View file

@ -18,16 +18,16 @@ struct Reaction;
class DocumentMedia;
} // namespace Data
namespace Lottie {
class Icon;
} // namespace Lottie
namespace Ui {
class AnimatedIcon;
} // namespace Ui
namespace HistoryView::Reactions {
struct ChosenReaction {
FullMsgId context;
Data::ReactionId id;
std::shared_ptr<Lottie::Icon> icon;
std::shared_ptr<Ui::AnimatedIcon> icon;
QRect geometry;
explicit operator bool() const {
@ -35,7 +35,7 @@ struct ChosenReaction {
}
};
using IconFactory = Fn<std::shared_ptr<Lottie::Icon>(
using IconFactory = Fn<std::shared_ptr<Ui::AnimatedIcon>(
not_null<Data::DocumentMedia*>,
int)>;
@ -84,14 +84,14 @@ private:
struct ReactionDocument {
std::shared_ptr<Data::DocumentMedia> media;
std::shared_ptr<Lottie::Icon> icon;
std::shared_ptr<Ui::AnimatedIcon> icon;
};
struct ReactionIcons {
ReactionId id;
DocumentData *appearAnimation = nullptr;
DocumentData *selectAnimation = nullptr;
std::shared_ptr<Lottie::Icon> appear;
std::shared_ptr<Lottie::Icon> select;
std::shared_ptr<Ui::AnimatedIcon> appear;
std::shared_ptr<Ui::AnimatedIcon> select;
mutable Ui::Animations::Simple selectedScale;
AddedButton added = AddedButton::None;
bool appearAnimated = false;
@ -133,7 +133,7 @@ private:
mutable int _selectedIcon = -1;
std::shared_ptr<Data::DocumentMedia> _mainReactionMedia;
std::shared_ptr<Lottie::Icon> _mainReactionIcon;
std::shared_ptr<Ui::AnimatedIcon> _mainReactionIcon;
QImage _mainReactionImage;
rpl::lifetime _mainReactionLifetime;
@ -153,11 +153,11 @@ public:
private:
base::flat_map<
std::shared_ptr<Data::DocumentMedia>,
std::shared_ptr<Lottie::Icon>> _cache;
std::shared_ptr<Ui::AnimatedIcon>> _cache;
};
[[nodiscard]] std::shared_ptr<Lottie::Icon> DefaultIconFactory(
[[nodiscard]] std::shared_ptr<Ui::AnimatedIcon> DefaultIconFactory(
not_null<Data::DocumentMedia*> media,
int size);

View file

@ -927,7 +927,7 @@ void SetupMessages(
const auto index = state->icons.flag ? 1 : 0;
state->icons.lifetimes[index] = rpl::lifetime();
const auto iconSize = st::settingsReactionRightIcon;
AddReactionLottieIcon(
AddReactionAnimatedIcon(
inner,
buttonRight->geometryValue(
) | rpl::map([=](const QRect &r) {

View file

@ -1,773 +0,0 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "ui/text/custom_emoji_instance.h"
#include "ui/effects/frame_generator.h"
#include "ui/ui_utility.h"
#include <crl/crl_async.h>
#include <lz4.h>
class QPainter;
namespace Ui::CustomEmoji {
namespace {
constexpr auto kMaxSize = 128;
constexpr auto kMaxFrames = 180;
constexpr auto kCacheVersion = 1;
constexpr auto kPreloadFrames = 3;
struct CacheHeader {
int version = 0;
int size = 0;
int frames = 0;
int length = 0;
};
void PaintScaledImage(
QPainter &p,
const QRect &target,
const Cache::Frame &frame,
const Context &context) {
if (context.scaled) {
const auto sx = anim::interpolate(
target.width() / 2,
0,
context.scale);
const auto sy = (target.height() == target.width())
? sx
: anim::interpolate(target.height() / 2, 0, context.scale);
const auto scaled = target.marginsRemoved({ sx, sy, sx, sy });
if (frame.source.isNull()) {
p.drawImage(scaled, *frame.image);
} else {
p.drawImage(scaled, *frame.image, frame.source);
}
} else if (frame.source.isNull()) {
p.drawImage(target, *frame.image);
} else {
p.drawImage(target, *frame.image, frame.source);
}
}
} // namespace
Preview::Preview(QPainterPath path, float64 scale)
: _data(ScaledPath{ std::move(path), scale }) {
}
Preview::Preview(QImage image, bool exact)
: _data(Image{ .data = std::move(image), .exact = exact }) {
}
void Preview::paint(QPainter &p, const Context &context) {
if (const auto path = std::get_if<ScaledPath>(&_data)) {
paintPath(p, context, *path);
} else if (const auto image = std::get_if<Image>(&_data)) {
const auto &data = image->data;
const auto factor = style::DevicePixelRatio();
const auto rect = QRect(context.position, data.size() / factor);
PaintScaledImage(p, rect, { .image = &data }, context);
}
}
bool Preview::isImage() const {
return v::is<Image>(_data);
}
bool Preview::isExactImage() const {
if (const auto image = std::get_if<Image>(&_data)) {
return image->exact;
}
return false;
}
QImage Preview::image() const {
if (const auto image = std::get_if<Image>(&_data)) {
return image->data;
}
return QImage();
}
void Preview::paintPath(
QPainter &p,
const Context &context,
const ScaledPath &path) {
auto hq = PainterHighQualityEnabler(p);
p.setBrush(context.preview);
p.setPen(Qt::NoPen);
const auto scale = path.scale;
const auto required = (scale != 1.) || context.scaled;
if (required) {
p.save();
}
p.translate(context.position);
if (required) {
p.scale(scale, scale);
const auto center = QPoint(
context.size.width() / 2,
context.size.height() / 2);
if (context.scaled) {
p.translate(center);
p.scale(context.scale, context.scale);
p.translate(-center);
}
}
p.drawPath(path.path);
if (required) {
p.restore();
} else {
p.translate(-context.position);
}
}
Cache::Cache(int size) : _size(size) {
}
std::optional<Cache> Cache::FromSerialized(
const QByteArray &serialized,
int requestedSize) {
Expects(requestedSize > 0 && requestedSize <= kMaxSize);
if (serialized.size() <= sizeof(CacheHeader)) {
return {};
}
auto header = CacheHeader();
memcpy(&header, serialized.data(), sizeof(header));
const auto size = header.size;
if (size != requestedSize
|| header.frames <= 0
|| header.frames >= kMaxFrames
|| header.length <= 0
|| header.length > (size * size * header.frames * sizeof(int32))
|| (serialized.size() != sizeof(CacheHeader)
+ header.length
+ (header.frames * sizeof(Cache(0)._durations[0])))) {
return {};
}
const auto rows = (header.frames + kPerRow - 1) / kPerRow;
const auto columns = std::min(header.frames, kPerRow);
auto durations = std::vector<uint16>(header.frames, 0);
auto full = QImage(
columns * size,
rows * size,
QImage::Format_ARGB32_Premultiplied);
Assert(full.bytesPerLine() == full.width() * sizeof(int32));
const auto decompressed = LZ4_decompress_safe(
serialized.data() + sizeof(CacheHeader),
reinterpret_cast<char*>(full.bits()),
header.length,
full.bytesPerLine() * full.height());
if (decompressed <= 0) {
return {};
}
memcpy(
durations.data(),
serialized.data() + sizeof(CacheHeader) + header.length,
header.frames * sizeof(durations[0]));
auto result = Cache(size);
result._finished = true;
result._full = std::move(full);
result._frames = header.frames;
result._durations = std::move(durations);
return result;
}
QByteArray Cache::serialize() {
Expects(_finished);
Expects(_durations.size() == _frames);
Expects(_full.bytesPerLine() == sizeof(int32) * _full.width());
auto header = CacheHeader{
.version = kCacheVersion,
.size = _size,
.frames = _frames,
};
const auto input = _full.width() * _full.height() * sizeof(int32);
const auto max = sizeof(CacheHeader)
+ LZ4_compressBound(input)
+ (_frames * sizeof(_durations[0]));
auto result = QByteArray(max, Qt::Uninitialized);
header.length = LZ4_compress_default(
reinterpret_cast<const char*>(_full.constBits()),
result.data() + sizeof(CacheHeader),
input,
result.size() - sizeof(CacheHeader));
Assert(header.length > 0);
memcpy(result.data(), &header, sizeof(CacheHeader));
memcpy(
result.data() + sizeof(CacheHeader) + header.length,
_durations.data(),
_frames * sizeof(_durations[0]));
result.resize(sizeof(CacheHeader)
+ header.length
+ _frames * sizeof(_durations[0]));
return result;
}
int Cache::frames() const {
return _frames;
}
Cache::Frame Cache::frame(int index) const {
Expects(index < _frames);
const auto row = index / kPerRow;
const auto inrow = index % kPerRow;
if (_finished) {
return { &_full, { inrow * _size, row * _size, _size, _size } };
}
return { &_images[row], { 0, inrow * _size, _size, _size } };
}
int Cache::size() const {
return _size;
}
Preview Cache::makePreview() const {
Expects(_frames > 0);
const auto first = frame(0);
return { first.image->copy(first.source), true };
}
void Cache::reserve(int frames) {
Expects(!_finished);
const auto rows = (frames + kPerRow - 1) / kPerRow;
if (const auto add = rows - int(_images.size()); add > 0) {
_images.resize(rows);
for (auto e = end(_images), i = e - add; i != e; ++i) {
(*i) = QImage(
_size,
_size * kPerRow,
QImage::Format_ARGB32_Premultiplied);
}
}
_durations.reserve(frames);
}
int Cache::frameRowByteSize() const {
return _size * 4;
}
int Cache::frameByteSize() const {
return _size * frameRowByteSize();
}
void Cache::add(crl::time duration, const QImage &frame) {
Expects(!_finished);
Expects(frame.size() == QSize(_size, _size));
Expects(frame.format() == QImage::Format_ARGB32_Premultiplied);
const auto row = (_frames / kPerRow);
const auto inrow = (_frames % kPerRow);
const auto rows = row + 1;
while (_images.size() < rows) {
_images.emplace_back();
_images.back() = QImage(
_size,
_size * kPerRow,
QImage::Format_ARGB32_Premultiplied);
}
const auto srcPerLine = frame.bytesPerLine();
const auto dstPerLine = _images[row].bytesPerLine();
const auto perLine = std::min(srcPerLine, dstPerLine);
auto dst = _images[row].bits() + inrow * _size * dstPerLine;
auto src = frame.constBits();
for (auto y = 0; y != _size; ++y) {
memcpy(dst, src, perLine);
dst += dstPerLine;
src += srcPerLine;
}
++_frames;
_durations.push_back(std::clamp(
duration,
crl::time(0),
crl::time(std::numeric_limits<uint16>::max())));
}
void Cache::finish() {
_finished = true;
if (_frame == _frames) {
_frame = 0;
}
const auto rows = (_frames + kPerRow - 1) / kPerRow;
const auto columns = std::min(_frames, kPerRow);
const auto zero = (rows * columns) - _frames;
_full = QImage(
columns * _size,
rows * _size,
QImage::Format_ARGB32_Premultiplied);
auto dstData = _full.bits();
const auto perLine = _size * 4;
const auto dstPerLine = _full.bytesPerLine();
for (auto y = 0; y != rows; ++y) {
auto &row = _images[y];
auto src = row.bits();
const auto srcPerLine = row.bytesPerLine();
const auto till = columns - ((y + 1 == rows) ? zero : 0);
for (auto x = 0; x != till; ++x) {
auto dst = dstData + y * dstPerLine * _size + x * perLine;
for (auto line = 0; line != _size; ++line) {
memcpy(dst, src, perLine);
src += srcPerLine;
dst += dstPerLine;
}
}
}
if (const auto perLine = zero * _size) {
auto dst = dstData
+ (rows - 1) * dstPerLine * _size
+ (columns - zero) * _size * 4;
for (auto left = 0; left != _size; ++left) {
memset(dst, 0, perLine);
dst += dstPerLine;
}
}
}
PaintFrameResult Cache::paintCurrentFrame(
QPainter &p,
const Context &context) {
if (!_frames) {
return {};
}
const auto first = context.firstFrameOnly;
if (!first) {
const auto now = context.paused ? 0 : context.now;
const auto finishes = now ? currentFrameFinishes() : 0;
if (finishes && now >= finishes) {
++_frame;
if (_finished && _frame == _frames) {
_frame = 0;
}
_shown = now;
} else if (!_shown) {
_shown = now;
}
}
const auto index = first ? 0 : std::min(_frame, _frames - 1);
const auto info = frame(index);
const auto size = _size / style::DevicePixelRatio();
const auto rect = QRect(context.position, QSize(size, size));
PaintScaledImage(p, rect, info, context);
const auto next = first ? 0 : currentFrameFinishes();
return {
.painted = true,
.next = next,
.duration = next ? (next - _shown) : 0,
};
}
int Cache::currentFrame() const {
return _frame;
}
crl::time Cache::currentFrameFinishes() const {
if (!_shown || _frame >= _durations.size()) {
return 0;
} else if (const auto duration = _durations[_frame]) {
return _shown + duration;
}
return 0;
}
Cached::Cached(
const QString &entityData,
Fn<std::unique_ptr<Loader>()> unloader,
Cache cache)
: _unloader(std::move(unloader))
, _cache(std::move(cache))
, _entityData(entityData) {
}
QString Cached::entityData() const {
return _entityData;
}
PaintFrameResult Cached::paint(QPainter &p, const Context &context) {
return _cache.paintCurrentFrame(p, context);
}
Preview Cached::makePreview() const {
return _cache.makePreview();
}
Loading Cached::unload() {
return Loading(_unloader(), makePreview());
}
Renderer::Renderer(RendererDescriptor &&descriptor)
: _cache(descriptor.size)
, _put(std::move(descriptor.put))
, _loader(std::move(descriptor.loader)) {
Expects(_loader != nullptr);
const auto size = _cache.size();
const auto guard = base::make_weak(this);
crl::async([=, factory = std::move(descriptor.generator)]() mutable {
auto generator = factory();
auto rendered = generator->renderNext(
QImage(),
QSize(size, size),
Qt::KeepAspectRatio);
if (rendered.image.isNull()) {
return;
}
crl::on_main(guard, [
=,
frame = std::move(rendered),
generator = std::move(generator)
]() mutable {
frameReady(
std::move(generator),
frame.duration,
std::move(frame.image));
});
});
}
Renderer::~Renderer() = default;
void Renderer::frameReady(
std::unique_ptr<Ui::FrameGenerator> generator,
crl::time duration,
QImage frame) {
if (frame.isNull()) {
finish();
return;
}
if (const auto count = generator->count()) {
if (!_cache.frames()) {
_cache.reserve(std::max(count, kMaxFrames));
}
}
const auto current = _cache.currentFrame();
const auto total = _cache.frames();
const auto explicitRepaint = (current == total);
_cache.add(duration, frame);
if (explicitRepaint && _repaint) {
_repaint();
}
if (!duration || total + 1 >= kMaxFrames) {
finish();
} else if (current + kPreloadFrames > total) {
renderNext(std::move(generator), std::move(frame));
} else {
_generator = std::move(generator);
_storage = std::move(frame);
}
}
void Renderer::renderNext(
std::unique_ptr<Ui::FrameGenerator> generator,
QImage storage) {
const auto size = _cache.size();
const auto guard = base::make_weak(this);
crl::async([
=,
storage = std::move(storage),
generator = std::move(generator)
]() mutable {
auto rendered = generator->renderNext(
std::move(storage),
QSize(size, size),
Qt::KeepAspectRatio);
crl::on_main(guard, [
=,
frame = std::move(rendered),
generator = std::move(generator)
]() mutable {
frameReady(
std::move(generator),
frame.duration,
std::move(frame.image));
});
});
}
void Renderer::finish() {
_finished = true;
_cache.finish();
if (_put) {
_put(_cache.serialize());
}
}
PaintFrameResult Renderer::paint(QPainter &p, const Context &context) {
const auto result = _cache.paintCurrentFrame(p, context);
if (_generator
&& (!result.painted
|| _cache.currentFrame() + kPreloadFrames >= _cache.frames())) {
renderNext(std::move(_generator), std::move(_storage));
}
return result;
}
std::optional<Cached> Renderer::ready(const QString &entityData) {
return _finished
? Cached{ entityData, std::move(_loader), std::move(_cache) }
: std::optional<Cached>();
}
std::unique_ptr<Loader> Renderer::cancel() {
return _loader();
}
bool Renderer::canMakePreview() const {
return _cache.frames() > 0;
}
Preview Renderer::makePreview() const {
return _cache.makePreview();
}
void Renderer::setRepaintCallback(Fn<void()> repaint) {
_repaint = std::move(repaint);
}
Cache Renderer::takeCache() {
return std::move(_cache);
}
Loading::Loading(std::unique_ptr<Loader> loader, Preview preview)
: _loader(std::move(loader))
, _preview(std::move(preview)) {
}
QString Loading::entityData() const {
return _loader->entityData();
}
void Loading::load(Fn<void(Loader::LoadResult)> done) {
_loader->load(crl::guard(this, [this, done = std::move(done)](
Loader::LoadResult result) mutable {
if (const auto caching = std::get_if<Caching>(&result)) {
caching->preview = _preview
? std::move(_preview)
: _loader->preview();
}
done(std::move(result));
}));
}
bool Loading::loading() const {
return _loader->loading();
}
void Loading::paint(QPainter &p, const Context &context) {
if (!_preview) {
if (auto preview = _loader->preview()) {
_preview = std::move(preview);
}
}
_preview.paint(p, context);
}
bool Loading::hasImagePreview() const {
return _preview.isImage();
}
Preview Loading::imagePreview() const {
return _preview.isImage() ? _preview : Preview();
}
void Loading::updatePreview(Preview preview) {
if (!_preview.isImage() && preview.isImage()) {
_preview = std::move(preview);
} else if (!_preview) {
if (auto loaderPreview = _loader->preview()) {
_preview = std::move(loaderPreview);
} else if (preview) {
_preview = std::move(preview);
}
}
}
void Loading::cancel() {
_loader->cancel();
invalidate_weak_ptrs(this);
}
Instance::Instance(
Loading loading,
Fn<void(not_null<Instance*>, RepaintRequest)> repaintLater)
: _state(std::move(loading))
, _repaintLater(std::move(repaintLater)) {
}
QString Instance::entityData() const {
return v::match(_state, [](const Loading &state) {
return state.entityData();
}, [](const Caching &state) {
return state.entityData;
}, [](const Cached &state) {
return state.entityData();
});
}
void Instance::paint(QPainter &p, const Context &context) {
v::match(_state, [&](Loading &state) {
state.paint(p, context);
load(state);
}, [&](Caching &state) {
auto result = state.renderer->paint(p, context);
if (!result.painted) {
state.preview.paint(p, context);
} else {
if (!state.preview.isExactImage()) {
state.preview = state.renderer->makePreview();
}
if (result.next > context.now) {
_repaintLater(this, { result.next, result.duration });
}
}
if (auto cached = state.renderer->ready(state.entityData)) {
_state = std::move(*cached);
}
}, [&](Cached &state) {
const auto result = state.paint(p, context);
if (result.next > context.now) {
_repaintLater(this, { result.next, result.duration });
}
});
}
bool Instance::ready() {
return v::match(_state, [&](Loading &state) {
if (state.hasImagePreview()) {
return true;
}
load(state);
return false;
}, [](Caching &state) {
return state.renderer->canMakePreview();
}, [](Cached &state) {
return true;
});
}
void Instance::load(Loading &state) {
state.load([=](Loader::LoadResult result) {
if (auto caching = std::get_if<Caching>(&result)) {
caching->renderer->setRepaintCallback([=] { repaint(); });
_state = std::move(*caching);
} else if (auto cached = std::get_if<Cached>(&result)) {
_state = std::move(*cached);
repaint();
} else {
Unexpected("Value in Loader::LoadResult.");
}
});
}
bool Instance::hasImagePreview() const {
return v::match(_state, [](const Loading &state) {
return state.hasImagePreview();
}, [](const Caching &state) {
return state.preview.isImage();
}, [](const Cached &state) {
return true;
});
}
Preview Instance::imagePreview() const {
return v::match(_state, [](const Loading &state) {
return state.imagePreview();
}, [](const Caching &state) {
return state.preview.isImage() ? state.preview : Preview();
}, [](const Cached &state) {
return state.makePreview();
});
}
void Instance::updatePreview(Preview preview) {
v::match(_state, [&](Loading &state) {
state.updatePreview(std::move(preview));
}, [&](Caching &state) {
if ((!state.preview.isImage() && preview.isImage())
|| (!state.preview && preview)) {
state.preview = std::move(preview);
}
}, [](const Cached &) {});
}
void Instance::repaint() {
for (const auto &object : _usage) {
object->repaint();
}
}
void Instance::incrementUsage(not_null<Object*> object) {
_usage.emplace(object);
}
void Instance::decrementUsage(not_null<Object*> object) {
_usage.remove(object);
if (!_usage.empty()) {
return;
}
v::match(_state, [](Loading &state) {
state.cancel();
}, [&](Caching &state) {
_state = Loading{
state.renderer->cancel(),
std::move(state.preview),
};
}, [&](Cached &state) {
_state = state.unload();
});
_repaintLater(this, RepaintRequest());
}
Object::Object(not_null<Instance*> instance, Fn<void()> repaint)
: _instance(instance)
, _repaint(std::move(repaint)) {
}
Object::~Object() {
unload();
}
QString Object::entityData() {
return _instance->entityData();
}
void Object::paint(QPainter &p, const Context &context) {
if (!_using) {
_using = true;
_instance->incrementUsage(this);
}
_instance->paint(p, context);
}
void Object::unload() {
if (_using) {
_using = false;
_instance->decrementUsage(this);
}
}
bool Object::ready() {
if (!_using) {
_using = true;
_instance->incrementUsage(this);
}
return _instance->ready();
}
void Object::repaint() {
_repaint();
}
} // namespace Ui::CustomEmoji

View file

@ -1,271 +0,0 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/text/text_custom_emoji.h"
#include "base/weak_ptr.h"
#include "base/bytes.h"
#include "base/timer.h"
class QColor;
class QPainter;
namespace Ui {
class FrameGenerator;
} // namespace Ui
namespace Ui::CustomEmoji {
using Context = Ui::Text::CustomEmoji::Context;
class Preview final {
public:
Preview() = default;
Preview(QImage image, bool exact);
Preview(QPainterPath path, float64 scale);
void paint(QPainter &p, const Context &context);
[[nodiscard]] bool isImage() const;
[[nodiscard]] bool isExactImage() const;
[[nodiscard]] QImage image() const;
[[nodiscard]] explicit operator bool() const {
return !v::is_null(_data);
}
private:
struct ScaledPath {
QPainterPath path;
float64 scale = 1.;
};
struct Image {
QImage data;
bool exact = false;
};
void paintPath(
QPainter &p,
const Context &context,
const ScaledPath &path);
std::variant<v::null_t, ScaledPath, Image> _data;
};
struct PaintFrameResult {
bool painted = false;
crl::time next = 0;
crl::time duration = 0;
};
class Cache final {
public:
Cache(int size);
struct Frame {
not_null<const QImage*> image;
QRect source;
};
[[nodiscard]] static std::optional<Cache> FromSerialized(
const QByteArray &serialized,
int requestedSize);
[[nodiscard]] QByteArray serialize();
[[nodiscard]] int size() const;
[[nodiscard]] int frames() const;
[[nodiscard]] Frame frame(int index) const;
void reserve(int frames);
void add(crl::time duration, const QImage &frame);
void finish();
[[nodiscard]] Preview makePreview() const;
PaintFrameResult paintCurrentFrame(QPainter &p, const Context &context);
[[nodiscard]] int currentFrame() const;
private:
static constexpr auto kPerRow = 16;
[[nodiscard]] int frameRowByteSize() const;
[[nodiscard]] int frameByteSize() const;
[[nodiscard]] crl::time currentFrameFinishes() const;
std::vector<QImage> _images;
std::vector<uint16> _durations;
QImage _full;
crl::time _shown = 0;
int _frame = 0;
int _size = 0;
int _frames = 0;
bool _finished = false;
};
class Loader;
class Loading;
class Cached final {
public:
Cached(
const QString &entityData,
Fn<std::unique_ptr<Loader>()> unloader,
Cache cache);
[[nodiscard]] QString entityData() const;
[[nodiscard]] Preview makePreview() const;
PaintFrameResult paint(QPainter &p, const Context &context);
[[nodiscard]] Loading unload();
private:
Fn<std::unique_ptr<Loader>()> _unloader;
Cache _cache;
QString _entityData;
};
struct RendererDescriptor {
Fn<std::unique_ptr<Ui::FrameGenerator>()> generator;
Fn<void(QByteArray)> put;
Fn<std::unique_ptr<Loader>()> loader;
int size = 0;
};
class Renderer final : public base::has_weak_ptr {
public:
explicit Renderer(RendererDescriptor &&descriptor);
virtual ~Renderer();
PaintFrameResult paint(QPainter &p, const Context &context);
[[nodiscard]] std::optional<Cached> ready(const QString &entityData);
[[nodiscard]] std::unique_ptr<Loader> cancel();
[[nodiscard]] bool canMakePreview() const;
[[nodiscard]] Preview makePreview() const;
void setRepaintCallback(Fn<void()> repaint);
[[nodiscard]] Cache takeCache();
private:
void frameReady(
std::unique_ptr<Ui::FrameGenerator> generator,
crl::time duration,
QImage frame);
void renderNext(
std::unique_ptr<Ui::FrameGenerator> generator,
QImage storage);
void finish();
Cache _cache;
std::unique_ptr<Ui::FrameGenerator> _generator;
QImage _storage;
Fn<void(QByteArray)> _put;
Fn<void()> _repaint;
Fn<std::unique_ptr<Loader>()> _loader;
bool _finished = false;
};
struct Caching {
std::unique_ptr<Renderer> renderer;
QString entityData;
Preview preview;
};
class Loader {
public:
using LoadResult = std::variant<Caching, Cached>;
[[nodiscard]] virtual QString entityData() = 0;
virtual void load(Fn<void(LoadResult)> loaded) = 0;
[[nodiscard]] virtual bool loading() = 0;
virtual void cancel() = 0;
[[nodiscard]] virtual Preview preview() = 0;
virtual ~Loader() = default;
};
class Loading final : public base::has_weak_ptr {
public:
Loading(std::unique_ptr<Loader> loader, Preview preview);
[[nodiscard]] QString entityData() const;
void load(Fn<void(Loader::LoadResult)> done);
[[nodiscard]] bool loading() const;
void paint(QPainter &p, const Context &context);
[[nodiscard]] bool hasImagePreview() const;
[[nodiscard]] Preview imagePreview() const;
void updatePreview(Preview preview);
void cancel();
private:
std::unique_ptr<Loader> _loader;
Preview _preview;
};
struct RepaintRequest {
crl::time when = 0;
crl::time duration = 0;
};
class Object;
class Instance final : public base::has_weak_ptr {
public:
Instance(
Loading loading,
Fn<void(not_null<Instance*>, RepaintRequest)> repaintLater);
Instance(const Instance&) = delete;
Instance &operator=(const Instance&) = delete;
[[nodiscard]] QString entityData() const;
void paint(QPainter &p, const Context &context);
[[nodiscard]] bool ready();
[[nodiscard]] bool hasImagePreview() const;
[[nodiscard]] Preview imagePreview() const;
void updatePreview(Preview preview);
void incrementUsage(not_null<Object*> object);
void decrementUsage(not_null<Object*> object);
void repaint();
private:
void load(Loading &state);
std::variant<Loading, Caching, Cached> _state;
base::flat_set<not_null<Object*>> _usage;
Fn<void(not_null<Instance*> that, RepaintRequest)> _repaintLater;
};
class Delegate {
public:
[[nodiscard]] virtual bool paused() = 0;
virtual ~Delegate() = default;
};
class Object final : public Ui::Text::CustomEmoji {
public:
Object(not_null<Instance*> instance, Fn<void()> repaint);
~Object();
QString entityData() override;
void paint(QPainter &p, const Context &context) override;
void unload() override;
bool ready() override;
void repaint();
private:
const not_null<Instance*> _instance;
Fn<void()> _repaint;
bool _using = false;
};
} // namespace Ui::CustomEmoji

View file

@ -10,8 +10,8 @@ init_target(lib_ffmpeg)
nice_target_sources(lib_ffmpeg ${src_loc}
PRIVATE
ffmpeg/ffmpeg_emoji.cpp
ffmpeg/ffmpeg_emoji.h
ffmpeg/ffmpeg_frame_generator.cpp
ffmpeg/ffmpeg_frame_generator.h
ffmpeg/ffmpeg_utility.cpp
ffmpeg/ffmpeg_utility.h
)

View file

@ -242,8 +242,6 @@ 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

@ -1 +1 @@
Subproject commit d4764ed8bf0ba2309847bca99f7f8b3d58db25b4
Subproject commit 960473ef42f92a4e0f0f2ac37a2c7b225f1527fc

@ -1 +1 @@
Subproject commit f876d15eedbce39a445b020b92f45d137952ed2a
Subproject commit 3287bf45c607e980268090ae27dfe4fb69d037e4