Support multiple reaction animations in one message.

This commit is contained in:
John Preston 2022-01-28 15:13:49 +03:00
parent 4f1e04cf9e
commit 8bde488662
10 changed files with 149 additions and 78 deletions

View file

@ -313,19 +313,22 @@ void BottomInfo::paintReactions(
int top, int top,
int availableWidth, int availableWidth,
const PaintContext &context) const { const PaintContext &context) const {
struct SingleAnimation {
not_null<Reactions::Animation*> animation;
QRect target;
};
std::vector<SingleAnimation> animations;
auto x = left; auto x = left;
auto y = top; auto y = top;
auto widthLeft = availableWidth; auto widthLeft = availableWidth;
const auto animated = _reactionAnimation
? _reactionAnimation->playingAroundEmoji()
: QString();
if (_reactionAnimation
&& context.reactionInfo
&& animated.isEmpty()) {
_reactionAnimation = nullptr;
}
for (const auto &reaction : _reactions) { for (const auto &reaction : _reactions) {
const auto animating = (reaction.emoji == animated); if (context.reactionInfo
&& reaction.animation
&& reaction.animation->finished()) {
reaction.animation = nullptr;
}
const auto animating = (reaction.animation != nullptr);
const auto add = (reaction.countTextWidth > 0) const auto add = (reaction.countTextWidth > 0)
? st::reactionInfoDigitSkip ? st::reactionInfoDigitSkip
: st::reactionInfoBetween; : st::reactionInfoBetween;
@ -349,14 +352,15 @@ void BottomInfo::paintReactions(
st::reactionInfoImage, st::reactionInfoImage,
st::reactionInfoImage); st::reactionInfoImage);
const auto skipImage = animating const auto skipImage = animating
&& (reaction.count < 2 || !_reactionAnimation->flying()); && (reaction.count < 2 || !reaction.animation->flying());
if (!reaction.image.isNull() && !skipImage) { if (!reaction.image.isNull() && !skipImage) {
p.drawImage(image.topLeft(), reaction.image); p.drawImage(image.topLeft(), reaction.image);
} }
if (animating) { if (animating) {
context.reactionInfo->effectPaint = [=](QPainter &p) { animations.push_back({
return _reactionAnimation->paintGetArea(p, origin, image); .animation = reaction.animation.get(),
}; .target = image,
});
} }
if (reaction.countTextWidth > 0) { if (reaction.countTextWidth > 0) {
p.drawText( p.drawText(
@ -367,6 +371,19 @@ void BottomInfo::paintReactions(
x += width + add; x += width + add;
widthLeft -= width + add; widthLeft -= width + add;
} }
if (!animations.empty()) {
context.reactionInfo->effectPaint = [=](QPainter &p) {
auto result = QRect();
for (const auto &single : animations) {
const auto area = single.animation->paintGetArea(
p,
origin,
single.target);
result = result.isEmpty() ? area : result.united(area);
}
return result;
};
}
} }
QSize BottomInfo::countCurrentSize(int newWidth) { QSize BottomInfo::countCurrentSize(int newWidth) {
@ -440,11 +457,6 @@ void BottomInfo::layoutRepliesText() {
} }
void BottomInfo::layoutReactionsText() { void BottomInfo::layoutReactionsText() {
if (_reactionAnimation
&& !_data.reactions.contains(
_reactionAnimation->playingAroundEmoji())) {
_reactionAnimation = nullptr;
}
if (_data.reactions.empty()) { if (_data.reactions.empty()) {
_reactions.clear(); _reactions.clear();
return; return;
@ -515,21 +527,39 @@ void BottomInfo::setReactionCount(Reaction &reaction, int count) {
void BottomInfo::animateReaction( void BottomInfo::animateReaction(
ReactionAnimationArgs &&args, ReactionAnimationArgs &&args,
Fn<void()> repaint) { Fn<void()> repaint) {
_reactionAnimation = std::make_unique<Reactions::Animation>( const auto i = ranges::find(_reactions, args.emoji, &Reaction::emoji);
if (i == end(_reactions)) {
return;
}
i->animation = std::make_unique<Reactions::Animation>(
_reactionsOwner, _reactionsOwner,
args.translated(QPoint(width(), height())), args.translated(QPoint(width(), height())),
std::move(repaint), std::move(repaint),
st::reactionInfoImage); st::reactionInfoImage);
} }
auto BottomInfo::takeSendReactionAnimation() auto BottomInfo::takeReactionAnimations()
-> std::unique_ptr<Reactions::Animation> { -> base::flat_map<QString, std::unique_ptr<Reactions::Animation>> {
return std::move(_reactionAnimation); auto result = base::flat_map<
QString,
std::unique_ptr<Reactions::Animation>>();
for (auto &reaction : _reactions) {
if (reaction.animation) {
result.emplace(reaction.emoji, std::move(reaction.animation));
}
}
return result;
} }
void BottomInfo::continueSendReactionAnimation( void BottomInfo::continueReactionAnimations(base::flat_map<
std::unique_ptr<Reactions::Animation> animation) { QString,
_reactionAnimation = std::move(animation); std::unique_ptr<Reactions::Animation>> animations) {
for (auto &[emoji, animation] : animations) {
const auto i = ranges::find(_reactions, emoji, &Reaction::emoji);
if (i != end(_reactions)) {
i->animation = std::move(animation);
}
}
} }
BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) { BottomInfo::Data BottomInfoDataFromMessage(not_null<Message*> message) {

View file

@ -76,13 +76,15 @@ public:
void animateReaction( void animateReaction(
ReactionAnimationArgs &&args, ReactionAnimationArgs &&args,
Fn<void()> repaint); Fn<void()> repaint);
[[nodiscard]] auto takeSendReactionAnimation() [[nodiscard]] auto takeReactionAnimations()
-> std::unique_ptr<Reactions::Animation>; -> base::flat_map<QString, std::unique_ptr<Reactions::Animation>>;
void continueSendReactionAnimation( void continueReactionAnimations(base::flat_map<
std::unique_ptr<Reactions::Animation> animation); QString,
std::unique_ptr<Reactions::Animation>> animations);
private: private:
struct Reaction { struct Reaction {
mutable std::unique_ptr<Reactions::Animation> animation;
mutable QImage image; mutable QImage image;
QString emoji; QString emoji;
QString countText; QString countText;
@ -124,7 +126,6 @@ private:
Ui::Text::String _replies; Ui::Text::String _replies;
std::vector<Reaction> _reactions; std::vector<Reaction> _reactions;
mutable ClickHandlerPtr _revokeLink; mutable ClickHandlerPtr _revokeLink;
mutable std::unique_ptr<Reactions::Animation> _reactionAnimation;
int _reactionsMaxWidth = 0; int _reactionsMaxWidth = 0;
int _dateWidth = 0; int _dateWidth = 0;
bool _authorElided = false; bool _authorElided = false;

View file

@ -1072,9 +1072,9 @@ void Element::animateUnreadReactions() {
} }
} }
auto Element::takeSendReactionAnimation() auto Element::takeReactionAnimations()
-> std::unique_ptr<Reactions::Animation> { -> base::flat_map<QString, std::unique_ptr<Reactions::Animation>> {
return nullptr; return {};
} }
Element::~Element() { Element::~Element() {

View file

@ -423,8 +423,8 @@ public:
virtual void animateReaction(ReactionAnimationArgs &&args); virtual void animateReaction(ReactionAnimationArgs &&args);
void animateUnreadReactions(); void animateUnreadReactions();
[[nodiscard]] virtual auto takeSendReactionAnimation() [[nodiscard]] virtual auto takeReactionAnimations()
-> std::unique_ptr<Reactions::Animation>; -> base::flat_map<QString, std::unique_ptr<Reactions::Animation>>;
virtual ~Element(); virtual ~Element();

View file

@ -253,15 +253,18 @@ Message::Message(
initLogEntryOriginal(); initLogEntryOriginal();
initPsa(); initPsa();
refreshReactions(); refreshReactions();
auto animation = replacing auto animations = replacing
? replacing->takeSendReactionAnimation() ? replacing->takeReactionAnimations()
: nullptr; : base::flat_map<QString, std::unique_ptr<Reactions::Animation>>();
if (animation) { if (!animations.empty()) {
animation->setRepaintCallback([=] { repaint(); }); const auto repainter = [=] { repaint(); };
for (const auto &[emoji, animation] : animations) {
animation->setRepaintCallback(repainter);
}
if (_reactions) { if (_reactions) {
_reactions->continueSendAnimation(std::move(animation)); _reactions->continueAnimations(std::move(animations));
} else { } else {
_bottomInfo.continueSendReactionAnimation(std::move(animation)); _bottomInfo.continueReactionAnimations(std::move(animations));
} }
} }
} }
@ -424,11 +427,11 @@ void Message::animateReaction(ReactionAnimationArgs &&args) {
} }
} }
auto Message::takeSendReactionAnimation() auto Message::takeReactionAnimations()
-> std::unique_ptr<Reactions::Animation> { -> base::flat_map<QString, std::unique_ptr<Reactions::Animation>> {
return _reactions return _reactions
? _reactions->takeSendAnimation() ? _reactions->takeAnimations()
: _bottomInfo.takeSendReactionAnimation(); : _bottomInfo.takeReactionAnimations();
} }
QSize Message::performCountOptimalSize() { QSize Message::performCountOptimalSize() {

View file

@ -136,8 +136,8 @@ public:
const base::flat_set<UserId> &changes) override; const base::flat_set<UserId> &changes) override;
void animateReaction(ReactionAnimationArgs &&args) override; void animateReaction(ReactionAnimationArgs &&args) override;
auto takeSendReactionAnimation() auto takeReactionAnimations()
-> std::unique_ptr<Reactions::Animation> override; -> base::flat_map<QString, std::unique_ptr<Reactions::Animation>> override;
protected: protected:
void refreshDataIdHook() override; void refreshDataIdHook() override;

View file

@ -27,11 +27,10 @@ Animation::Animation(
Fn<void()> repaint, Fn<void()> repaint,
int size) int size)
: _owner(owner) : _owner(owner)
, _emoji(args.emoji)
, _repaint(std::move(repaint)) , _repaint(std::move(repaint))
, _flyFrom(args.flyFrom) { , _flyFrom(args.flyFrom) {
const auto &list = owner->list(::Data::Reactions::Type::All); const auto &list = owner->list(::Data::Reactions::Type::All);
const auto i = ranges::find(list, _emoji, &::Data::Reaction::emoji); const auto i = ranges::find(list, args.emoji, &::Data::Reaction::emoji);
if (i == end(list) || !i->centerIcon) { if (i == end(list) || !i->centerIcon) {
return; return;
} }
@ -172,10 +171,9 @@ float64 Animation::flyingProgress() const {
return _fly.value(1.); return _fly.value(1.);
} }
QString Animation::playingAroundEmoji() const { bool Animation::finished() const {
const auto finished = !_valid return !_valid
|| (!_flyIcon && !_center->animating() && !_effect->animating()); || (!_flyIcon && !_center->animating() && !_effect->animating());
return finished ? QString() : _emoji;
} }
} // namespace HistoryView::Reactions } // namespace HistoryView::Reactions

View file

@ -35,9 +35,9 @@ public:
void setRepaintCallback(Fn<void()> repaint); void setRepaintCallback(Fn<void()> repaint);
QRect paintGetArea(QPainter &p, QPoint origin, QRect target) const; QRect paintGetArea(QPainter &p, QPoint origin, QRect target) const;
[[nodiscard]] QString playingAroundEmoji() const;
[[nodiscard]] bool flying() const; [[nodiscard]] bool flying() const;
[[nodiscard]] float64 flyingProgress() const; [[nodiscard]] float64 flyingProgress() const;
[[nodiscard]] bool finished() const;
private: private:
void flyCallback(); void flyCallback();
@ -46,7 +46,6 @@ private:
int computeParabolicTop(int from, int to, float64 progress) const; int computeParabolicTop(int from, int to, float64 progress) const;
const not_null<::Data::Reactions*> _owner; const not_null<::Data::Reactions*> _owner;
const QString _emoji;
Fn<void()> _repaint; Fn<void()> _repaint;
std::shared_ptr<Lottie::Icon> _flyIcon; std::shared_ptr<Lottie::Icon> _flyIcon;
std::unique_ptr<Lottie::Icon> _center; std::unique_ptr<Lottie::Icon> _center;

View file

@ -268,29 +268,34 @@ void InlineList::paint(
const PaintContext &context, const PaintContext &context,
int outerWidth, int outerWidth,
const QRect &clip) const { const QRect &clip) const {
struct SingleAnimation {
not_null<Reactions::Animation*> animation;
QRect target;
};
std::vector<SingleAnimation> animations;
const auto st = context.st; const auto st = context.st;
const auto stm = context.messageStyle(); const auto stm = context.messageStyle();
const auto padding = st::reactionInlinePadding; const auto padding = st::reactionInlinePadding;
const auto size = st::reactionInlineSize; const auto size = st::reactionInlineSize;
const auto skip = (size - st::reactionInlineImage) / 2; const auto skip = (size - st::reactionInlineImage) / 2;
const auto inbubble = (_data.flags & InlineListData::Flag::InBubble); const auto inbubble = (_data.flags & InlineListData::Flag::InBubble);
const auto animated = (_animation && context.reactionInfo)
? _animation->playingAroundEmoji()
: QString();
const auto flipped = (_data.flags & Data::Flag::Flipped); const auto flipped = (_data.flags & Data::Flag::Flipped);
if (_animation && context.reactionInfo && animated.isEmpty()) {
_animation = nullptr;
}
p.setFont(st::semiboldFont); p.setFont(st::semiboldFont);
for (const auto &button : _buttons) { for (const auto &button : _buttons) {
if (context.reactionInfo
&& button.animation
&& button.animation->finished()) {
button.animation = nullptr;
}
const auto animating = (button.animation != nullptr);
const auto &geometry = button.geometry; const auto &geometry = button.geometry;
const auto mine = (_data.chosenReaction == button.emoji); const auto mine = (_data.chosenReaction == button.emoji);
const auto withoutMine = button.count - (mine ? 1 : 0); const auto withoutMine = button.count - (mine ? 1 : 0);
const auto animating = (animated == button.emoji);
const auto skipImage = animating const auto skipImage = animating
&& (withoutMine < 1 || !_animation->flying()); && (withoutMine < 1 || !button.animation->flying());
const auto bubbleProgress = skipImage const auto bubbleProgress = skipImage
? _animation->flyingProgress() ? button.animation->flyingProgress()
: 1.; : 1.;
const auto bubbleReady = (bubbleProgress == 1.); const auto bubbleReady = (bubbleProgress == 1.);
const auto bubbleSkip = anim::interpolate( const auto bubbleSkip = anim::interpolate(
@ -299,7 +304,7 @@ void InlineList::paint(
bubbleProgress); bubbleProgress);
const auto inner = geometry.marginsRemoved(padding); const auto inner = geometry.marginsRemoved(padding);
const auto chosen = mine const auto chosen = mine
&& (!animating || !_animation->flying() || skipImage); && (!animating || !button.animation->flying() || skipImage);
if (bubbleProgress > 0.) { if (bubbleProgress > 0.) {
auto hq = PainterHighQualityEnabler(p); auto hq = PainterHighQualityEnabler(p);
p.setPen(Qt::NoPen); p.setPen(Qt::NoPen);
@ -342,9 +347,10 @@ void InlineList::paint(
p.drawImage(image.topLeft(), button.image); p.drawImage(image.topLeft(), button.image);
} }
if (animating) { if (animating) {
context.reactionInfo->effectPaint = [=](QPainter &p) { animations.push_back({
return _animation->paintGetArea(p, QPoint(), image); .animation = button.animation.get(),
}; .target = image,
});
} }
if (bubbleProgress == 0.) { if (bubbleProgress == 0.) {
continue; continue;
@ -381,6 +387,19 @@ void InlineList::paint(
p.setOpacity(1.); p.setOpacity(1.);
} }
} }
if (!animations.empty()) {
context.reactionInfo->effectPaint = [=](QPainter &p) {
auto result = QRect();
for (const auto &single : animations) {
const auto area = single.animation->paintGetArea(
p,
QPoint(),
single.target);
result = result.isEmpty() ? area : result.united(area);
}
return result;
};
}
} }
bool InlineList::getState( bool InlineList::getState(
@ -411,7 +430,11 @@ bool InlineList::getState(
void InlineList::animate( void InlineList::animate(
ReactionAnimationArgs &&args, ReactionAnimationArgs &&args,
Fn<void()> repaint) { Fn<void()> repaint) {
_animation = std::make_unique<Reactions::Animation>( const auto i = ranges::find(_buttons, args.emoji, &Button::emoji);
if (i == end(_buttons)) {
return;
}
i->animation = std::make_unique<Reactions::Animation>(
_owner, _owner,
std::move(args), std::move(args),
std::move(repaint), std::move(repaint),
@ -447,13 +470,28 @@ void InlineList::resolveUserpicsImage(const Button &button) const {
kMaxRecentUserpics); kMaxRecentUserpics);
} }
std::unique_ptr<Animation> InlineList::takeSendAnimation() { auto InlineList::takeAnimations()
return std::move(_animation); -> base::flat_map<QString, std::unique_ptr<Reactions::Animation>> {
auto result = base::flat_map<
QString,
std::unique_ptr<Reactions::Animation>>();
for (auto &button : _buttons) {
if (button.animation) {
result.emplace(button.emoji, std::move(button.animation));
}
}
return result;
} }
void InlineList::continueSendAnimation( void InlineList::continueAnimations(base::flat_map<
std::unique_ptr<Animation> animation) { QString,
_animation = std::move(animation); std::unique_ptr<Reactions::Animation>> animations) {
for (auto &[emoji, animation] : animations) {
const auto i = ranges::find(_buttons, emoji, &Button::emoji);
if (i != end(_buttons)) {
i->animation = std::move(animation);
}
}
} }
InlineListData InlineListDataFromMessage(not_null<Message*> message) { InlineListData InlineListDataFromMessage(not_null<Message*> message) {

View file

@ -75,8 +75,11 @@ public:
void animate( void animate(
ReactionAnimationArgs &&args, ReactionAnimationArgs &&args,
Fn<void()> repaint); Fn<void()> repaint);
[[nodiscard]] std::unique_ptr<Animation> takeSendAnimation(); [[nodiscard]] auto takeAnimations()
void continueSendAnimation(std::unique_ptr<Animation> animation); -> base::flat_map<QString, std::unique_ptr<Reactions::Animation>>;
void continueAnimations(base::flat_map<
QString,
std::unique_ptr<Reactions::Animation>> animations);
private: private:
struct Userpics { struct Userpics {
@ -86,6 +89,7 @@ private:
}; };
struct Button { struct Button {
QRect geometry; QRect geometry;
mutable std::unique_ptr<Animation> animation;
mutable QImage image; mutable QImage image;
mutable ClickHandlerPtr link; mutable ClickHandlerPtr link;
std::unique_ptr<Userpics> userpics; std::unique_ptr<Userpics> userpics;
@ -113,8 +117,6 @@ private:
std::vector<Button> _buttons; std::vector<Button> _buttons;
QSize _skipBlock; QSize _skipBlock;
mutable std::unique_ptr<Animation> _animation;
}; };
[[nodiscard]] InlineListData InlineListDataFromMessage( [[nodiscard]] InlineListData InlineListDataFromMessage(