Fade in/out effect preview.

This commit is contained in:
John Preston 2024-05-14 14:07:38 +04:00
parent 8a58ded582
commit 487fa9728a
6 changed files with 155 additions and 64 deletions

View file

@ -575,7 +575,9 @@ void Reactions::preloadReactionImageFor(const ReactionId &emoji) {
} }
void Reactions::preloadEffectImageFor(EffectId id) { void Reactions::preloadEffectImageFor(EffectId id) {
preloadImageFor({ DocumentId(id) }); if (id != kFakeEffectId) {
preloadImageFor({ DocumentId(id) });
}
} }
void Reactions::preloadImageFor(const ReactionId &id) { void Reactions::preloadImageFor(const ReactionId &id) {
@ -651,7 +653,9 @@ QImage Reactions::resolveReactionImageFor(const ReactionId &emoji) {
} }
QImage Reactions::resolveEffectImageFor(EffectId id) { QImage Reactions::resolveEffectImageFor(EffectId id) {
return resolveImageFor({ DocumentId(id) }); return (id == kFakeEffectId)
? QImage()
: resolveImageFor({ DocumentId(id) });
} }
QImage Reactions::resolveImageFor(const ReactionId &id) { QImage Reactions::resolveImageFor(const ReactionId &id) {

View file

@ -119,6 +119,10 @@ public:
void preloadReactionImageFor(const ReactionId &emoji); void preloadReactionImageFor(const ReactionId &emoji);
[[nodiscard]] QImage resolveReactionImageFor(const ReactionId &emoji); [[nodiscard]] QImage resolveReactionImageFor(const ReactionId &emoji);
// This is used to reserve space for the effect in BottomInfo but not
// actually paint anything, used in case we want to paint icon ourselves.
static constexpr auto kFakeEffectId = EffectId(1);
void preloadEffectImageFor(EffectId id); void preloadEffectImageFor(EffectId id);
[[nodiscard]] QImage resolveEffectImageFor(EffectId id); [[nodiscard]] QImage resolveEffectImageFor(EffectId id);

View file

@ -203,6 +203,7 @@ Selector::Selector(
TextWithEntities about, TextWithEntities about,
Fn<void(bool fast)> close, Fn<void(bool fast)> close,
IconFactory iconFactory, IconFactory iconFactory,
Fn<bool()> paused,
bool child) bool child)
: Selector( : Selector(
parent, parent,
@ -216,8 +217,9 @@ Selector::Selector(
: ChatHelpers::EmojiListMode::MessageEffects), : ChatHelpers::EmojiListMode::MessageEffects),
{}, {},
std::move(about), std::move(about),
iconFactory, std::move(iconFactory),
close, std::move(paused),
std::move(close),
child) { child) {
} }
@ -253,6 +255,7 @@ Selector::Selector(
std::vector<DocumentId> recent, std::vector<DocumentId> recent,
TextWithEntities about, TextWithEntities about,
IconFactory iconFactory, IconFactory iconFactory,
Fn<bool()> paused,
Fn<void(bool fast)> close, Fn<void(bool fast)> close,
bool child) bool child)
: RpWidget(parent) : RpWidget(parent)
@ -261,6 +264,7 @@ Selector::Selector(
, _reactions(reactions) , _reactions(reactions)
, _recent(std::move(recent)) , _recent(std::move(recent))
, _listMode(mode) , _listMode(mode)
, _paused(std::move(paused))
, _jumpedToPremium([=] { close(false); }) , _jumpedToPremium([=] { close(false); })
, _cachedRound( , _cachedRound(
QSize(2 * st::reactStripSkip + st::reactStripSize, st::reactStripHeight), QSize(2 * st::reactStripSkip + st::reactStripSize, st::reactStripHeight),
@ -1005,7 +1009,7 @@ void Selector::createList() {
object_ptr<EmojiListWidget>(lists, EmojiListDescriptor{ object_ptr<EmojiListWidget>(lists, EmojiListDescriptor{
.show = _show, .show = _show,
.mode = _listMode, .mode = _listMode,
.paused = [] { return false; }, .paused = _paused ? _paused : [] { return false; },
.customRecentList = std::move(recentList), .customRecentList = std::move(recentList),
.customRecentFactory = _unifiedFactoryOwner->factory(), .customRecentFactory = _unifiedFactoryOwner->factory(),
.freeEffects = std::move(freeEffects), .freeEffects = std::move(freeEffects),
@ -1026,7 +1030,7 @@ void Selector::createList() {
StickersListDescriptor{ StickersListDescriptor{
.show = _show, .show = _show,
.mode = StickersListMode::MessageEffects, .mode = StickersListMode::MessageEffects,
.paused = [] { return false; }, .paused = _paused ? _paused : [] { return false; },
.customRecentList = std::move(descriptors), .customRecentList = std::move(descriptors),
.st = st, .st = st,
})); }));
@ -1352,7 +1356,8 @@ auto AttachSelectorToMenu(
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
const Data::PossibleItemReactionsRef &reactions, const Data::PossibleItemReactionsRef &reactions,
TextWithEntities about, TextWithEntities about,
IconFactory iconFactory) IconFactory iconFactory,
Fn<bool()> paused)
-> base::expected<not_null<Selector*>, AttachSelectorResult> { -> base::expected<not_null<Selector*>, AttachSelectorResult> {
if (reactions.recent.empty()) { if (reactions.recent.empty()) {
return base::make_unexpected(AttachSelectorResult::Skipped); return base::make_unexpected(AttachSelectorResult::Skipped);
@ -1366,6 +1371,7 @@ auto AttachSelectorToMenu(
std::move(about), std::move(about),
[=](bool fast) { menu->hideMenu(fast); }, [=](bool fast) { menu->hideMenu(fast); },
std::move(iconFactory), std::move(iconFactory),
std::move(paused),
false); // child false); // child
if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) { if (!AdjustMenuGeometryForSelector(menu, desiredPosition, selector)) {
return base::make_unexpected(AttachSelectorResult::Failed); return base::make_unexpected(AttachSelectorResult::Failed);

View file

@ -85,6 +85,7 @@ public:
TextWithEntities about, TextWithEntities about,
Fn<void(bool fast)> close, Fn<void(bool fast)> close,
IconFactory iconFactory = nullptr, IconFactory iconFactory = nullptr,
Fn<bool()> paused = nullptr,
bool child = false); bool child = false);
#if 0 // not ready #if 0 // not ready
Selector( Selector(
@ -149,6 +150,7 @@ private:
std::vector<DocumentId> recent, std::vector<DocumentId> recent,
TextWithEntities about, TextWithEntities about,
IconFactory iconFactory, IconFactory iconFactory,
Fn<bool()> paused,
Fn<void(bool fast)> close, Fn<void(bool fast)> close,
bool child); bool child);
@ -187,6 +189,7 @@ private:
const Data::PossibleItemReactions _reactions; const Data::PossibleItemReactions _reactions;
const std::vector<DocumentId> _recent; const std::vector<DocumentId> _recent;
const ChatHelpers::EmojiListMode _listMode; const ChatHelpers::EmojiListMode _listMode;
const Fn<bool()> _paused;
Fn<void()> _jumpedToPremium; Fn<void()> _jumpedToPremium;
Ui::RoundAreaWithShadow _cachedRound; Ui::RoundAreaWithShadow _cachedRound;
std::unique_ptr<Strip> _strip; std::unique_ptr<Strip> _strip;
@ -274,7 +277,8 @@ AttachSelectorResult AttachSelectorToMenu(
std::shared_ptr<ChatHelpers::Show> show, std::shared_ptr<ChatHelpers::Show> show,
const Data::PossibleItemReactionsRef &reactions, const Data::PossibleItemReactionsRef &reactions,
TextWithEntities about, TextWithEntities about,
IconFactory iconFactory = nullptr IconFactory iconFactory = nullptr,
Fn<bool()> paused = nullptr
) -> base::expected<not_null<Selector*>, AttachSelectorResult>; ) -> base::expected<not_null<Selector*>, AttachSelectorResult>;
[[nodiscard]] TextWithEntities ItemReactionsAbout( [[nodiscard]] TextWithEntities ItemReactionsAbout(

View file

@ -668,7 +668,8 @@ void Reactions::Panel::create() {
? tr::lng_stories_reaction_as_message(tr::now) ? tr::lng_stories_reaction_as_message(tr::now)
: QString()) }, : QString()) },
[=](bool fast) { hide(mode); }, [=](bool fast) { hide(mode); },
nullptr, nullptr, // iconFactory
nullptr, // paused
true); true);
_selector->chosen( _selector->chosen(

View file

@ -59,6 +59,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
namespace SendMenu { namespace SendMenu {
namespace { namespace {
constexpr auto kToggleDuration = crl::time(400);
class Delegate final : public HistoryView::DefaultElementDelegate { class Delegate final : public HistoryView::DefaultElementDelegate {
public: public:
Delegate(not_null<Ui::PathShiftGradient*> pathGradient) Delegate(not_null<Ui::PathShiftGradient*> pathGradient)
@ -90,6 +92,8 @@ public:
Fn<void(Action, Details)> action, Fn<void(Action, Details)> action,
Fn<void()> done); Fn<void()> done);
void hideAnimated();
private: private:
void paintEvent(QPaintEvent *e) override; void paintEvent(QPaintEvent *e) override;
void mousePressEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override;
@ -104,8 +108,12 @@ private:
void setupSend(Details details); void setupSend(Details details);
void createLottie(); void createLottie();
[[nodiscard]] bool ready() const;
void paintLoading(QPainter &p);
void paintLottie(QPainter &p);
bool checkIconBecameLoaded(); bool checkIconBecameLoaded();
[[nodiscard]] bool checkReady(); [[nodiscard]] bool checkLoaded();
void toggle(bool shown);
const EffectId _effectId = 0; const EffectId _effectId = 0;
const Data::Reaction _effect; const Data::Reaction _effect;
@ -135,6 +143,10 @@ private:
QRect _iconRect; QRect _iconRect;
std::unique_ptr<Ui::InfiniteRadialAnimation> _loading; std::unique_ptr<Ui::InfiniteRadialAnimation> _loading;
Ui::Animations::Simple _shownAnimation;
QPixmap _bottomCache;
bool _hiding = false;
rpl::lifetime _readyCheckLifetime; rpl::lifetime _readyCheckLifetime;
}; };
@ -248,7 +260,7 @@ EffectPreview::EffectPreview(
_history->peer->id, _history->peer->id,
_replyTo->data()->fullId(), _replyTo->data()->fullId(),
tr::lng_settings_chat_message_reply(tr::now), tr::lng_settings_chat_message_reply(tr::now),
_effectId)) Data::Reactions::kFakeEffectId))
, _send(canSend() , _send(canSend()
? std::make_unique<BottomRounded>( ? std::make_unique<BottomRounded>(
this, this,
@ -271,68 +283,87 @@ EffectPreview::EffectPreview(
, _close(done) , _close(done)
, _actionWithEffect(ComposeActionWithEffect(action, _effectId, done)) { , _actionWithEffect(ComposeActionWithEffect(action, _effectId, done)) {
setupGeometry(position); setupGeometry(position);
setupBackground();
setupItem(); setupItem();
setupBackground();
setupLottie(); setupLottie();
setupSend(details); setupSend(details);
toggle(true);
} }
void EffectPreview::paintEvent(QPaintEvent *e) { void EffectPreview::paintEvent(QPaintEvent *e) {
auto p = Painter(this); checkIconBecameLoaded();
const auto progress = _shownAnimation.value(_hiding ? 0. : 1.);
if (!progress) {
return;
}
auto p = QPainter(this);
p.setOpacity(progress);
p.drawImage(0, 0, _bg); p.drawImage(0, 0, _bg);
p.setClipRect(_inner); if (!_bottomCache.isNull()) {
p.translate(_itemShift); p.drawPixmap(_bottom->pos(), _bottomCache);
auto rect = QRect(0, 0, st::windowMinWidth, _inner.height()); }
auto context = _theme->preparePaintContext(
_chatStyle.get(),
rect,
rect,
false);
context.outbg = _item->hasOutLayout();
_item->draw(p, context);
p.translate(-_itemShift);
checkIconBecameLoaded(); if (!ready()) {
if (_icon.isNull()) { paintLoading(p);
if (!_loading) {
_loading = std::make_unique<Ui::InfiniteRadialAnimation>([=] {
update();
}, st::effectPreviewLoading);
_loading->start(st::defaultInfiniteRadialAnimation.linearPeriod);
}
const auto loading = _iconRect.marginsRemoved(
{ st::lineWidth, st::lineWidth, st::lineWidth, st::lineWidth });
auto hq = PainterHighQualityEnabler(p);
Ui::InfiniteRadialAnimation::Draw(
p,
_loading->computeState(),
loading.topLeft(),
loading.size(),
width(),
_chatStyle->msgInDateFg(),
st::effectPreviewLoading.thickness);
} else { } else {
_loading = nullptr; _loading = nullptr;
} p.drawImage(_iconRect, _icon);
if (_lottie && _lottie->ready()) { if (!_hiding) {
const auto factor = style::DevicePixelRatio(); p.setOpacity(1.);
auto request = Lottie::FrameRequest();
request.box = _inner.size() * factor;
const auto rightAligned = _item->hasRightLayout();
if (!rightAligned) {
request.mirrorHorizontal = true;
} }
const auto frame = _lottie->frameInfo(request); paintLottie(p);
p.drawImage(
QRect(_inner.topLeft(), frame.image.size() / factor),
frame.image);
_lottie->markFrameShown();
} }
} }
bool EffectPreview::ready() const {
return !_icon.isNull() && _lottie && _lottie->ready();
}
void EffectPreview::paintLoading(QPainter &p) {
if (!_loading) {
_loading = std::make_unique<Ui::InfiniteRadialAnimation>([=] {
update();
}, st::effectPreviewLoading);
_loading->start(st::defaultInfiniteRadialAnimation.linearPeriod);
}
const auto loading = _iconRect.marginsRemoved(
{ st::lineWidth, st::lineWidth, st::lineWidth, st::lineWidth });
auto hq = PainterHighQualityEnabler(p);
Ui::InfiniteRadialAnimation::Draw(
p,
_loading->computeState(),
loading.topLeft(),
loading.size(),
width(),
_chatStyle->msgInDateFg(),
st::effectPreviewLoading.thickness);
}
void EffectPreview::paintLottie(QPainter &p) {
const auto factor = style::DevicePixelRatio();
auto request = Lottie::FrameRequest();
request.box = _inner.size() * factor;
const auto rightAligned = _item->hasRightLayout();
if (!rightAligned) {
request.mirrorHorizontal = true;
}
const auto frame = _lottie->frameInfo(request);
p.drawImage(
QRect(_inner.topLeft(), frame.image.size() / factor),
frame.image);
_lottie->markFrameShown();
}
void EffectPreview::hideAnimated() {
toggle(false);
}
void EffectPreview::mousePressEvent(QMouseEvent *e) { void EffectPreview::mousePressEvent(QMouseEvent *e) {
delete this; hideAnimated();
} }
void EffectPreview::setupGeometry(QPoint position) { void EffectPreview::setupGeometry(QPoint position) {
@ -402,7 +433,7 @@ void EffectPreview::repaintBackground() {
bg.setDevicePixelRatio(ratio); bg.setDevicePixelRatio(ratio);
{ {
auto p = QPainter(&bg); auto p = Painter(&bg);
Window::SectionWidget::PaintBackground( Window::SectionWidget::PaintBackground(
p, p,
_theme.get(), _theme.get(),
@ -411,6 +442,18 @@ void EffectPreview::repaintBackground() {
p.fillRect( p.fillRect(
QRect(0, _inner.height(), _inner.width(), _bottom->height()), QRect(0, _inner.height(), _inner.width(), _bottom->height()),
st::previewMarkRead.bgColor); st::previewMarkRead.bgColor);
p.translate(_itemShift - _inner.topLeft());
auto rect = QRect(0, 0, st::windowMinWidth, _inner.height());
auto context = _theme->preparePaintContext(
_chatStyle.get(),
rect,
rect,
false);
context.outbg = _item->hasOutLayout();
_item->draw(p, context);
p.translate(_inner.topLeft() - _itemShift);
auto hq = PainterHighQualityEnabler(p); auto hq = PainterHighQualityEnabler(p);
p.setCompositionMode(QPainter::CompositionMode_DestinationIn); p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
auto roundRect = Ui::RoundRect(st::previewMenu.radius, st::menuBg); auto roundRect = Ui::RoundRect(st::previewMenu.radius, st::menuBg);
@ -438,7 +481,7 @@ void EffectPreview::setupLottie() {
rpl::single(rpl::empty) | rpl::then( rpl::single(rpl::empty) | rpl::then(
_show->session().downloaderTaskFinished() _show->session().downloaderTaskFinished()
) | rpl::start_with_next([=] { ) | rpl::start_with_next([=] {
if (checkReady()) { if (checkLoaded()) {
_readyCheckLifetime.destroy(); _readyCheckLifetime.destroy();
createLottie(); createLottie();
} }
@ -495,10 +538,14 @@ bool EffectPreview::checkIconBecameLoaded() {
} }
const auto reactions = &_show->session().data().reactions(); const auto reactions = &_show->session().data().reactions();
_icon = reactions->resolveEffectImageFor(_effect.id.custom()); _icon = reactions->resolveEffectImageFor(_effect.id.custom());
return !_icon.isNull(); if (_icon.isNull()) {
return false;
}
repaintBackground();
return true;
} }
bool EffectPreview::checkReady() { bool EffectPreview::checkLoaded() {
if (checkIconBecameLoaded()) { if (checkIconBecameLoaded()) {
update(); update();
} }
@ -511,6 +558,29 @@ bool EffectPreview::checkReady() {
return !_icon.isNull() && (!_bytes.isEmpty() || !_filepath.isEmpty()); return !_icon.isNull() && (!_bytes.isEmpty() || !_filepath.isEmpty());
} }
void EffectPreview::toggle(bool shown) {
if (!shown && _hiding) {
return;
}
_hiding = !shown;
if (_bottomCache.isNull()) {
_bottomCache = Ui::GrabWidget(_bottom);
_bottom->hide();
}
_shownAnimation.start([=] {
update();
if (!_shownAnimation.animating()) {
if (_hiding) {
delete this;
} else {
_bottomCache = QPixmap();
_bottom->show();
}
}
}, shown ? 0. : 1., shown ? 1. : 0., kToggleDuration, anim::easeOutCirc);
show();
}
} // namespace } // namespace
Fn<void(Action, Details)> DefaultCallback( Fn<void(Action, Details)> DefaultCallback(
@ -571,6 +641,7 @@ FillMenuResult FillSendMenu(
} }
using namespace HistoryView::Reactions; using namespace HistoryView::Reactions;
const auto effect = std::make_shared<QPointer<EffectPreview>>();
const auto position = desiredPositionOverride.value_or(QCursor::pos()); const auto position = desiredPositionOverride.value_or(QCursor::pos());
const auto selector = (showForEffect && details.effectAllowed) const auto selector = (showForEffect && details.effectAllowed)
? AttachSelectorToMenu( ? AttachSelectorToMenu(
@ -579,7 +650,9 @@ FillMenuResult FillSendMenu(
st::reactPanelEmojiPan, st::reactPanelEmojiPan,
showForEffect, showForEffect,
LookupPossibleEffects(&showForEffect->session()), LookupPossibleEffects(&showForEffect->session()),
{ tr::lng_effect_add_title(tr::now) }) { tr::lng_effect_add_title(tr::now) },
nullptr, // iconFactory
[=] { return (*effect) != nullptr; }) // paused
: base::make_unexpected(AttachSelectorResult::Skipped); : base::make_unexpected(AttachSelectorResult::Skipped);
if (!selector) { if (!selector) {
if (selector.error() == AttachSelectorResult::Failed) { if (selector.error() == AttachSelectorResult::Failed) {
@ -589,7 +662,6 @@ FillMenuResult FillSendMenu(
return FillMenuResult::Prepared; return FillMenuResult::Prepared;
} }
const auto effect = std::make_shared<QPointer<EffectPreview>>();
(*selector)->chosen( (*selector)->chosen(
) | rpl::start_with_next([=](ChosenReaction chosen) { ) | rpl::start_with_next([=](ChosenReaction chosen) {
const auto &reactions = showForEffect->session().data().reactions(); const auto &reactions = showForEffect->session().data().reactions();
@ -597,7 +669,7 @@ FillMenuResult FillSendMenu(
const auto i = ranges::find(effects, chosen.id, &Data::Reaction::id); const auto i = ranges::find(effects, chosen.id, &Data::Reaction::id);
if (i != end(effects)) { if (i != end(effects)) {
if (const auto strong = effect->data()) { if (const auto strong = effect->data()) {
delete strong; strong->hideAnimated();
} }
const auto weak = Ui::MakeWeak(menu); const auto weak = Ui::MakeWeak(menu);
const auto done = [=] { const auto done = [=] {