572 lines
14 KiB
C++
572 lines
14 KiB
C++
/*
|
|
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 "history/view/reactions/history_view_reactions_strip.h"
|
|
|
|
#include "data/data_message_reactions.h"
|
|
#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 "styles/style_chat.h"
|
|
|
|
namespace HistoryView::Reactions {
|
|
namespace {
|
|
|
|
constexpr auto kSizeForDownscale = 96;
|
|
constexpr auto kEmojiCacheIndex = 0;
|
|
constexpr auto kHoverScaleDuration = crl::time(200);
|
|
constexpr auto kHoverScale = 1.24;
|
|
|
|
[[nodiscard]] int MainReactionSize() {
|
|
return style::ConvertScale(kSizeForDownscale);
|
|
}
|
|
|
|
[[nodiscard]] std::shared_ptr<Lottie::Icon> CreateIcon(
|
|
not_null<Data::DocumentMedia*> media,
|
|
int size,
|
|
int frame) {
|
|
Expects(media->loaded());
|
|
|
|
return std::make_shared<Lottie::Icon>(Lottie::IconDescriptor{
|
|
.path = media->owner()->filepath(true),
|
|
.json = media->bytes(),
|
|
.sizeOverride = QSize(size, size),
|
|
.frame = frame,
|
|
.limitFps = true,
|
|
});
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Strip::Strip(
|
|
QRect inner,
|
|
int size,
|
|
Fn<void()> update,
|
|
IconFactory iconFactory)
|
|
: _iconFactory(std::move(iconFactory))
|
|
, _inner(inner)
|
|
, _finalSize(size)
|
|
, _update(std::move(update)) {
|
|
}
|
|
|
|
void Strip::applyList(
|
|
const std::vector<not_null<const Data::Reaction*>> &list,
|
|
AddedButton button) {
|
|
if (_button == button
|
|
&& ranges::equal(
|
|
ranges::make_subrange(
|
|
begin(_icons),
|
|
(begin(_icons)
|
|
+ _icons.size()
|
|
- (_button == AddedButton::None ? 0 : 1))),
|
|
list,
|
|
ranges::equal_to(),
|
|
&ReactionIcons::id,
|
|
&Data::Reaction::id)) {
|
|
return;
|
|
}
|
|
const auto selected = _selectedIcon;
|
|
setSelected(-1);
|
|
_icons.clear();
|
|
for (const auto &reaction : list) {
|
|
_icons.push_back({
|
|
.id = reaction->id,
|
|
.appearAnimation = reaction->appearAnimation,
|
|
.selectAnimation = reaction->selectAnimation,
|
|
});
|
|
}
|
|
_button = button;
|
|
if (_button != AddedButton::None) {
|
|
_icons.push_back({ .added = _button });
|
|
}
|
|
setSelected((selected < _icons.size()) ? selected : -1);
|
|
resolveMainReactionIcon();
|
|
}
|
|
|
|
void Strip::paint(
|
|
QPainter &p,
|
|
QPoint position,
|
|
QPoint shift,
|
|
QRect clip,
|
|
float64 scale,
|
|
bool hiding) {
|
|
const auto skip = st::reactionAppearStartSkip;
|
|
const auto animationRect = clip.marginsRemoved({ 0, skip, 0, skip });
|
|
|
|
PainterHighQualityEnabler hq(p);
|
|
const auto countTarget = resolveCountTargetMethod(scale);
|
|
for (auto &icon : _icons) {
|
|
const auto target = countTarget(icon).translated(position);
|
|
position += shift;
|
|
if (target.intersects(clip)) {
|
|
paintOne(
|
|
p,
|
|
icon,
|
|
position - shift,
|
|
target,
|
|
!hiding && target.intersects(animationRect));
|
|
} else if (!hiding) {
|
|
clearStateForHidden(icon);
|
|
}
|
|
if (!hiding) {
|
|
clearStateForSelectFinished(icon);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto Strip::resolveCountTargetMethod(float64 scale) const
|
|
-> Fn<QRectF(const ReactionIcons&)> {
|
|
const auto hoveredSize = int(base::SafeRound(_finalSize * kHoverScale));
|
|
const auto basicTargetForScale = [&](int size, float64 scale) {
|
|
const auto remove = size * (1. - scale) / 2.;
|
|
return QRectF(QRect(
|
|
_inner.x() + (_inner.width() - size) / 2,
|
|
_inner.y() + (_inner.height() - size) / 2,
|
|
size,
|
|
size
|
|
)).marginsRemoved({ remove, remove, remove, remove });
|
|
};
|
|
const auto basicTarget = basicTargetForScale(_finalSize, scale);
|
|
return [=](const ReactionIcons &icon) {
|
|
const auto selectScale = icon.selectedScale.value(
|
|
icon.selected ? kHoverScale : 1.);
|
|
if (selectScale == 1.) {
|
|
return basicTarget;
|
|
}
|
|
const auto finalScale = scale * selectScale;
|
|
return (finalScale <= 1.)
|
|
? basicTargetForScale(_finalSize, finalScale)
|
|
: basicTargetForScale(hoveredSize, finalScale / kHoverScale);
|
|
};
|
|
}
|
|
|
|
void Strip::paintOne(
|
|
QPainter &p,
|
|
ReactionIcons &icon,
|
|
QPoint position,
|
|
QRectF target,
|
|
bool allowAppearStart) {
|
|
if (icon.added == AddedButton::Premium) {
|
|
paintPremiumIcon(p, position, target);
|
|
} else if (icon.added == AddedButton::Expand) {
|
|
paintExpandIcon(p, position, target);
|
|
} else {
|
|
const auto paintFrame = [&](not_null<Lottie::Icon*> animation) {
|
|
const auto size = int(std::floor(target.width() + 0.01));
|
|
const auto frame = animation->frame({ size, size }, _update);
|
|
p.drawImage(target, frame.image);
|
|
};
|
|
|
|
const auto appear = icon.appear.get();
|
|
if (appear && !icon.appearAnimated && allowAppearStart) {
|
|
icon.appearAnimated = true;
|
|
appear->animate(_update, 0, appear->framesCount() - 1);
|
|
}
|
|
if (appear && appear->animating()) {
|
|
paintFrame(appear);
|
|
} else if (const auto select = icon.select.get()) {
|
|
paintFrame(select);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Strip::paintOne(
|
|
QPainter &p,
|
|
int index,
|
|
QPoint position,
|
|
float64 scale) {
|
|
Expects(index >= 0 && index < _icons.size());
|
|
|
|
auto &icon = _icons[index];
|
|
const auto countTarget = resolveCountTargetMethod(scale);
|
|
const auto target = countTarget(icon).translated(position);
|
|
paintOne(p, icon, position, target, false);
|
|
}
|
|
|
|
bool Strip::inDefaultState(int index) const {
|
|
Expects(index >= 0 && index < _icons.size());
|
|
|
|
const auto &icon = _icons[index];
|
|
return !icon.selected
|
|
&& !icon.selectedScale.animating()
|
|
&& icon.select
|
|
&& !icon.select->animating()
|
|
&& (!icon.appear || !icon.appear->animating());
|
|
}
|
|
|
|
bool Strip::empty() const {
|
|
return _icons.empty();
|
|
}
|
|
|
|
int Strip::count() const {
|
|
return _icons.size();
|
|
}
|
|
|
|
bool Strip::onlyAddedButton() const {
|
|
return (_icons.size() == 1)
|
|
&& (_icons.front().added != AddedButton::None);
|
|
}
|
|
|
|
int Strip::fillChosenIconGetIndex(ChosenReaction &chosen) const {
|
|
const auto i = ranges::find(_icons, chosen.id, &ReactionIcons::id);
|
|
if (i == end(_icons)) {
|
|
return -1;
|
|
}
|
|
const auto &icon = *i;
|
|
if (const auto &appear = icon.appear; appear && appear->animating()) {
|
|
chosen.icon = CreateIcon(
|
|
icon.appearAnimation->activeMediaView().get(),
|
|
appear->width(),
|
|
appear->frameIndex());
|
|
} else if (const auto &select = icon.select) {
|
|
chosen.icon = CreateIcon(
|
|
icon.selectAnimation->activeMediaView().get(),
|
|
select->width(),
|
|
select->frameIndex());
|
|
}
|
|
return (i - begin(_icons));
|
|
}
|
|
|
|
void Strip::paintPremiumIcon(
|
|
QPainter &p,
|
|
QPoint position,
|
|
QRectF target) const {
|
|
const auto to = QRect(
|
|
_inner.x() + (_inner.width() - _finalSize) / 2,
|
|
_inner.y() + (_inner.height() - _finalSize) / 2,
|
|
_finalSize,
|
|
_finalSize
|
|
).translated(position);
|
|
const auto scale = target.width() / to.width();
|
|
if (scale != 1.) {
|
|
p.save();
|
|
p.translate(target.center());
|
|
p.scale(scale, scale);
|
|
p.translate(-target.center());
|
|
}
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
st::reactionPremiumLocked.paintInCenter(p, to);
|
|
if (scale != 1.) {
|
|
p.restore();
|
|
}
|
|
}
|
|
|
|
void Strip::paintExpandIcon(
|
|
QPainter &p,
|
|
QPoint position,
|
|
QRectF target) const {
|
|
const auto to = QRect(
|
|
_inner.x() + (_inner.width() - _finalSize) / 2,
|
|
_inner.y() + (_inner.height() - _finalSize) / 2,
|
|
_finalSize,
|
|
_finalSize
|
|
).translated(position);
|
|
const auto scale = target.width() / to.width();
|
|
if (scale != 1.) {
|
|
p.save();
|
|
p.translate(target.center());
|
|
p.scale(scale, scale);
|
|
p.translate(-target.center());
|
|
}
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
((_finalSize == st::reactionCornerImage)
|
|
? st::reactionsExpandDropdown
|
|
: st::reactionExpandPanel).paintInCenter(p, to);
|
|
if (scale != 1.) {
|
|
p.restore();
|
|
}
|
|
}
|
|
|
|
void Strip::setSelected(int index) const {
|
|
const auto set = [&](int index, bool selected) {
|
|
if (index < 0 || index >= _icons.size()) {
|
|
return;
|
|
}
|
|
auto &icon = _icons[index];
|
|
if (icon.selected == selected) {
|
|
return;
|
|
}
|
|
icon.selected = selected;
|
|
icon.selectedScale.start(
|
|
_update,
|
|
selected ? 1. : kHoverScale,
|
|
selected ? kHoverScale : 1.,
|
|
kHoverScaleDuration,
|
|
anim::sineInOut);
|
|
if (selected) {
|
|
const auto skipAnimation = icon.selectAnimated
|
|
|| !icon.appearAnimated
|
|
|| (icon.select && icon.select->animating())
|
|
|| (icon.appear && icon.appear->animating());
|
|
const auto select = skipAnimation ? nullptr : icon.select.get();
|
|
if (select && !icon.selectAnimated) {
|
|
icon.selectAnimated = true;
|
|
select->animate(_update, 0, select->framesCount() - 1);
|
|
}
|
|
}
|
|
};
|
|
if (_selectedIcon != index) {
|
|
set(_selectedIcon, false);
|
|
_selectedIcon = index;
|
|
}
|
|
set(index, true);
|
|
}
|
|
|
|
auto Strip::selected() const -> std::variant<AddedButton, ReactionId> {
|
|
if (_selectedIcon < 0 || _selectedIcon >= _icons.size()) {
|
|
return {};
|
|
}
|
|
const auto &icon = _icons[_selectedIcon];
|
|
if (icon.added != AddedButton::None) {
|
|
return icon.added;
|
|
}
|
|
return icon.id;
|
|
}
|
|
|
|
int Strip::computeOverSize() const {
|
|
return int(base::SafeRound(_finalSize * kHoverScale));
|
|
}
|
|
|
|
void Strip::clearAppearAnimations(bool mainAppeared) {
|
|
auto main = mainAppeared;
|
|
for (auto &icon : _icons) {
|
|
if (!main) {
|
|
if (icon.selected) {
|
|
setSelected(-1);
|
|
}
|
|
icon.selectedScale.stop();
|
|
if (const auto select = icon.select.get()) {
|
|
select->jumpTo(0, nullptr);
|
|
}
|
|
icon.selectAnimated = false;
|
|
}
|
|
if (icon.appearAnimated != main) {
|
|
if (const auto appear = icon.appear.get()) {
|
|
appear->jumpTo(0, nullptr);
|
|
}
|
|
icon.appearAnimated = main;
|
|
}
|
|
main = false;
|
|
}
|
|
}
|
|
|
|
void Strip::clearStateForHidden(ReactionIcons &icon) {
|
|
if (const auto appear = icon.appear.get()) {
|
|
appear->jumpTo(0, nullptr);
|
|
}
|
|
if (icon.selected) {
|
|
setSelected(-1);
|
|
}
|
|
icon.appearAnimated = false;
|
|
icon.selectAnimated = false;
|
|
if (const auto select = icon.select.get()) {
|
|
select->jumpTo(0, nullptr);
|
|
}
|
|
icon.selectedScale.stop();
|
|
}
|
|
|
|
void Strip::clearStateForSelectFinished(ReactionIcons &icon) {
|
|
if (icon.selectAnimated
|
|
&& !icon.select->animating()
|
|
&& !icon.selected) {
|
|
icon.selectAnimated = false;
|
|
}
|
|
}
|
|
|
|
bool Strip::checkIconLoaded(ReactionDocument &entry) const {
|
|
if (!entry.media) {
|
|
return true;
|
|
} else if (!entry.media->loaded()) {
|
|
return false;
|
|
}
|
|
const auto size = (entry.media == _mainReactionMedia)
|
|
? MainReactionSize()
|
|
: _finalSize;
|
|
entry.icon = _iconFactory(entry.media.get(), size);
|
|
entry.media = nullptr;
|
|
return true;
|
|
}
|
|
|
|
void Strip::loadIcons() {
|
|
const auto load = [&](not_null<DocumentData*> document) {
|
|
if (const auto i = _loadCache.find(document); i != end(_loadCache)) {
|
|
return i->second.icon;
|
|
}
|
|
auto &entry = _loadCache.emplace(document).first->second;
|
|
entry.media = document->createMediaView();
|
|
entry.media->checkStickerLarge();
|
|
if (!checkIconLoaded(entry) && !_loadCacheLifetime) {
|
|
document->session().downloaderTaskFinished(
|
|
) | rpl::start_with_next([=] {
|
|
checkIcons();
|
|
}, _loadCacheLifetime);
|
|
}
|
|
return entry.icon;
|
|
};
|
|
auto all = true;
|
|
for (auto &icon : _icons) {
|
|
if (icon.appearAnimation && !icon.appear) {
|
|
icon.appear = load(icon.appearAnimation);
|
|
if (!icon.appear) {
|
|
all = false;
|
|
}
|
|
}
|
|
if (icon.selectAnimation && !icon.select) {
|
|
icon.select = load(icon.selectAnimation);
|
|
if (!icon.select) {
|
|
all = false;
|
|
}
|
|
}
|
|
}
|
|
if (all && !_icons.empty() && _icons.front().appearAnimation) {
|
|
auto &data = _icons.front().appearAnimation->owner().reactions();
|
|
for (const auto &icon : _icons) {
|
|
data.preloadAnimationsFor(icon.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Strip::checkIcons() {
|
|
auto all = true;
|
|
for (auto &[document, entry] : _loadCache) {
|
|
if (!checkIconLoaded(entry)) {
|
|
all = false;
|
|
}
|
|
}
|
|
if (all) {
|
|
_loadCacheLifetime.destroy();
|
|
loadIcons();
|
|
}
|
|
}
|
|
|
|
void Strip::resolveMainReactionIcon() {
|
|
if (_icons.empty() || onlyAddedButton()) {
|
|
_mainReactionMedia = nullptr;
|
|
_mainReactionLifetime.destroy();
|
|
return;
|
|
}
|
|
const auto main = _icons.front().selectAnimation;
|
|
Assert(main != nullptr);
|
|
_icons.front().appearAnimated = true;
|
|
if (_mainReactionMedia && _mainReactionMedia->owner() == main) {
|
|
if (!_mainReactionLifetime) {
|
|
loadIcons();
|
|
}
|
|
return;
|
|
}
|
|
_mainReactionMedia = main->createMediaView();
|
|
_mainReactionMedia->checkStickerLarge();
|
|
if (_mainReactionMedia->loaded()) {
|
|
_mainReactionLifetime.destroy();
|
|
setMainReactionIcon();
|
|
} else if (!_mainReactionLifetime) {
|
|
main->session().downloaderTaskFinished(
|
|
) | rpl::filter([=] {
|
|
return _mainReactionMedia->loaded();
|
|
}) | rpl::take(1) | rpl::start_with_next([=] {
|
|
setMainReactionIcon();
|
|
}, _mainReactionLifetime);
|
|
}
|
|
}
|
|
|
|
void Strip::setMainReactionIcon() {
|
|
_mainReactionLifetime.destroy();
|
|
ranges::fill(_validEmoji, false);
|
|
loadIcons();
|
|
const auto i = _loadCache.find(_mainReactionMedia->owner());
|
|
if (i != end(_loadCache) && i->second.icon) {
|
|
const auto &icon = i->second.icon;
|
|
if (!icon->frameIndex() && icon->width() == MainReactionSize()) {
|
|
_mainReactionImage = i->second.icon->frame();
|
|
return;
|
|
}
|
|
}
|
|
_mainReactionImage = QImage();
|
|
_mainReactionIcon = DefaultIconFactory(
|
|
_mainReactionMedia.get(),
|
|
MainReactionSize());
|
|
}
|
|
|
|
bool Strip::onlyMainEmojiVisible() const {
|
|
if (_icons.empty()) {
|
|
return true;
|
|
}
|
|
const auto &icon = _icons.front();
|
|
if (icon.selected
|
|
|| icon.selectedScale.animating()
|
|
|| (icon.select && icon.select->animating())) {
|
|
return false;
|
|
}
|
|
icon.selectAnimated = false;
|
|
return true;
|
|
}
|
|
|
|
Ui::ImageSubrect Strip::validateEmoji(int frameIndex, float64 scale) {
|
|
const auto area = _inner.size();
|
|
const auto size = int(base::SafeRound(_finalSize * scale));
|
|
const auto result = Ui::ImageSubrect{
|
|
&_emojiParts,
|
|
Ui::RoundAreaWithShadow::FrameCacheRect(
|
|
frameIndex,
|
|
kEmojiCacheIndex,
|
|
area),
|
|
};
|
|
if (_validEmoji[frameIndex]) {
|
|
return result;
|
|
} else if (_emojiParts.isNull()) {
|
|
_emojiParts = Ui::RoundAreaWithShadow::PrepareFramesCache(area);
|
|
}
|
|
|
|
auto p = QPainter(result.image);
|
|
const auto ratio = style::DevicePixelRatio();
|
|
const auto position = result.rect.topLeft() / ratio;
|
|
p.setCompositionMode(QPainter::CompositionMode_Source);
|
|
p.fillRect(QRect(position, result.rect.size() / ratio), Qt::transparent);
|
|
if (_mainReactionImage.isNull()
|
|
&& _mainReactionIcon) {
|
|
_mainReactionImage = base::take(_mainReactionIcon)->frame();
|
|
}
|
|
if (!_mainReactionImage.isNull()) {
|
|
const auto target = QRect(
|
|
(_inner.width() - size) / 2,
|
|
(_inner.height() - size) / 2,
|
|
size,
|
|
size
|
|
).translated(position);
|
|
|
|
p.drawImage(target, _mainReactionImage.scaled(
|
|
target.size() * ratio,
|
|
Qt::IgnoreAspectRatio,
|
|
Qt::SmoothTransformation));
|
|
}
|
|
|
|
_validEmoji[frameIndex] = true;
|
|
return result;
|
|
}
|
|
|
|
IconFactory CachedIconFactory::createMethod() {
|
|
return [=](not_null<Data::DocumentMedia*> media, int size) {
|
|
const auto owned = media->owner()->createMediaView();
|
|
const auto i = _cache.find(owned);
|
|
return (i != end(_cache))
|
|
? i->second
|
|
: _cache.emplace(
|
|
owned,
|
|
DefaultIconFactory(media, size)).first->second;
|
|
};
|
|
}
|
|
|
|
std::shared_ptr<Lottie::Icon> DefaultIconFactory(
|
|
not_null<Data::DocumentMedia*> media,
|
|
int size) {
|
|
return CreateIcon(media, size, 0);
|
|
}
|
|
|
|
} // namespace HistoryView::Reactions
|