/* 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 class QPainter; namespace Ui::CustomEmoji { namespace { constexpr auto kMaxFrameDuration = 86400 * crl::time(1000); struct CacheHelper { int version = 0; int size = 0; int frames = 0; int length = 0; }; } // namespace Preview::Preview(QPainterPath path, float64 scale) : _data(ScaledPath{ std::move(path), scale }) { } 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(&_data)) { paintPath(p, x, y, preview, *path); } else if (const auto image = std::get_if(&_data)) { p.drawImage(x, y, *image); } } bool Preview::image() const { return v::is(_data); } 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(int size) : _size(size) { } std::optional Cache::FromSerialized(const QByteArray &serialized) { return {}; } QByteArray Cache::serialize() { return {}; } int Cache::frames() const { return _frames; } QImage Cache::frame(int index) const { 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(bytes); return QImage(data, _size, _size, QImage::Format_ARGB32_Premultiplied); } int Cache::size() const { return _size; } Preview Cache::makePreview() const { Expects(_frames > 0); auto image = frame(0); image.detach(); return { std::move(image) }; } void Cache::reserve(int frames) { const auto rows = (frames + kPerRow - 1) / kPerRow; if (const auto add = rows - int(_bytes.size()); add > 0) { _bytes.resize(rows); for (auto e = end(_bytes), i = e - add; i != e; ++i) { i->resize(kPerRow * frameByteSize()); } } _durations.reserve(frames); } int Cache::frameRowByteSize() const { return _size * 4; } int Cache::frameByteSize() const { return _size * frameRowByteSize(); } void Cache::add(crl::time duration, const QImage &frame) { Expects(duration < kMaxFrameDuration); Expects(frame.size() == QSize(_size, _size)); Expects(frame.format() == QImage::Format_ARGB32_Premultiplied); const auto rowSize = frameRowByteSize(); const auto frameSize = frameByteSize(); const auto row = (_frames / kPerRow); const auto inrow = (_frames % kPerRow); const auto rows = row + 1; while (_bytes.size() < rows) { _bytes.emplace_back(); _bytes.back().resize(kPerRow * frameSize); } const auto perLine = frame.bytesPerLine(); auto dst = _bytes[row].data() + inrow * frameSize; auto src = frame.constBits(); for (auto y = 0; y != _size; ++y) { memcpy(dst, src, rowSize); dst += rowSize; src += perLine; } ++_frames; _durations.push_back(duration); } void Cache::finish() { _finished = true; if (_frame == _frames) { _frame = 0; } } PaintFrameResult Cache::paintCurrentFrame( QPainter &p, int x, int y, crl::time now) { if (!_frames) { return {}; } const auto finishes = now ? currentFrameFinishes() : 0; if (finishes && now >= finishes) { ++_frame; if (_finished && _frame == _frames) { _frame = 0; } _shown = now; } else if (!_shown) { _shown = now; } p.drawImage( QRect(x, y, _size, _size), frame(std::min(_frame, _frames - 1))); const auto next = currentFrameFinishes(); const auto duration = next ? (next - _shown) : 0; return { .painted = true, .next = currentFrameFinishes(), .duration = duration, }; } 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()> 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 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 Renderer::ready(const QString &entityData) { return _finished ? Cached{ entityData, std::move(_loader), std::move(_cache) } : std::optional(); } std::unique_ptr Renderer::cancel() { return _loader(); } Preview Renderer::makePreview() const { return _cache.makePreview(); } void Renderer::setRepaintCallback(Fn repaint) { _repaint = std::move(repaint); } Cache Renderer::takeCache() { return std::move(_cache); } Loading::Loading(std::unique_ptr loader, Preview preview) : _loader(std::move(loader)) , _preview(std::move(preview)) { } QString Loading::entityData() const { return _loader->entityData(); } void Loading::load(Fn done) { _loader->load(crl::guard(this, [this, done = std::move(done)]( Loader::LoadResult result) mutable { if (const auto caching = std::get_if(&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) { 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( Loading loading, Fn, RepaintRequest)> repaintLater) : _state(std::move(loading)) , _repaintLater(std::move(repaintLater)) { } QString Instance::entityData() const { if (const auto loading = std::get_if(&_state)) { return loading->entityData(); } else if (const auto caching = std::get_if(&_state)) { return caching->entityData; } else if (const auto cached = std::get_if(&_state)) { return cached->entityData(); } Unexpected("State in Instance::entityData."); } void Instance::paint( QPainter &p, int x, int y, crl::time now, const QColor &preview, bool paused) { if (const auto loading = std::get_if(&_state)) { loading->paint(p, x, y, preview); loading->load([=](Loader::LoadResult result) { if (auto caching = std::get_if(&result)) { caching->renderer->setRepaintCallback([=] { repaint(); }); _state = std::move(*caching); } else if (auto cached = std::get_if(&result)) { _state = std::move(*cached); } else { Unexpected("Value in Loader::LoadResult."); } }); } else if (const auto caching = std::get_if(&_state)) { auto result = caching->renderer->paint(p, x, y, paused ? 0 : now); if (!result.painted) { caching->preview.paint(p, x, y, preview); } else { if (!caching->preview.image()) { caching->preview = caching->renderer->makePreview(); } if (result.next > now) { _repaintLater(this, { result.next, result.duration }); } } if (auto cached = caching->renderer->ready(caching->entityData)) { _state = std::move(*cached); } } else if (const auto cached = std::get_if(&_state)) { const auto result = cached->paint(p, x, y, paused ? 0 : now); if (result.next > now) { _repaintLater(this, { result.next, result.duration }); } } } void Instance::repaint() { for (const auto &object : _usage) { object->repaint(); } } void Instance::incrementUsage(not_null object) { _usage.emplace(object); } void Instance::decrementUsage(not_null object) { _usage.remove(object); if (!_usage.empty()) { return; } if (const auto loading = std::get_if(&_state)) { loading->cancel(); } else if (const auto caching = std::get_if(&_state)) { _state = Loading{ caching->renderer->cancel(), std::move(caching->preview), }; } else if (const auto cached = std::get_if(&_state)) { _state = cached->unload(); } _repaintLater(this, RepaintRequest()); } Object::Object(not_null instance, Fn repaint) : _instance(instance) , _repaint(std::move(repaint)) { } Object::~Object() { unload(); } QString Object::entityData() { return _instance->entityData(); } void Object::paint( QPainter &p, int x, int y, crl::time now, const QColor &preview, bool paused) { if (!_using) { _using = true; _instance->incrementUsage(this); } _instance->paint(p, x, y, now, preview, paused); } void Object::unload() { if (_using) { _using = false; _instance->decrementUsage(this); } } void Object::repaint() { _repaint(); } } // namespace Ui::CustomEmoji