tdesktop/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp
2023-10-28 18:06:17 +04:00

1555 lines
42 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 "chat_helpers/stickers_list_footer.h"
#include "chat_helpers/emoji_keywords.h"
#include "chat_helpers/stickers_emoji_pack.h"
#include "chat_helpers/stickers_lottie.h"
#include "core/application.h"
#include "data/stickers/data_stickers_set.h"
#include "data/stickers/data_stickers.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_file_origin.h"
#include "data/data_channel.h"
#include "data/data_session.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "main/main_account.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_single_player.h"
#include "ui/dpr/dpr_icon.h"
#include "ui/dpr/dpr_image.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/buttons.h"
#include "ui/painter.h"
#include "ui/rect_part.h"
#include "styles/style_chat_helpers.h"
#include <QtWidgets/QApplication>
namespace ChatHelpers {
namespace {
constexpr auto kEmojiSectionSetIdBase = uint64(0x77FF'FFFF'FFFF'FFF0ULL);
constexpr auto kEmojiSearchLimit = 32;
using EmojiSection = Ui::Emoji::Section;
void UpdateAnimated(anim::value &value, int to) {
if (int(base::SafeRound(value.to())) == to) {
return;
}
value = anim::value(
(value.from() != value.to()) ? value.from() : to,
to);
}
void UpdateAnimated(
anim::value &value,
int to,
ValidateIconAnimations animations) {
if (animations == ValidateIconAnimations::Full) {
value.start(to);
} else {
value = anim::value(to, to);
}
}
} // namespace
uint64 EmojiSectionSetId(EmojiSection section) {
Expects(section >= EmojiSection::Recent
&& section <= EmojiSection::Symbols);
return kEmojiSectionSetIdBase + static_cast<uint64>(section) + 1;
}
uint64 RecentEmojiSectionSetId() {
return EmojiSectionSetId(EmojiSection::Recent);
}
uint64 AllEmojiSectionSetId() {
return kEmojiSectionSetIdBase;
}
uint64 SearchEmojiSectionSetId() {
return kEmojiSectionSetIdBase
+ static_cast<uint64>(EmojiSection::Symbols)
+ 2;
}
std::optional<EmojiSection> SetIdEmojiSection(uint64 id) {
const auto base = RecentEmojiSectionSetId();
if (id < base) {
return {};
}
const auto index = id - base;
return (index <= uint64(EmojiSection::Symbols))
? static_cast<EmojiSection>(index)
: std::optional<EmojiSection>();
}
[[nodiscard]] std::vector<QString> GifSearchEmojiFallback() {
return {
u"\xf0\x9f\x91\x8d"_q,
u"\xf0\x9f\x98\x98"_q,
u"\xf0\x9f\x98\x8d"_q,
u"\xf0\x9f\x98\xa1"_q,
u"\xf0\x9f\xa5\xb3"_q,
u"\xf0\x9f\x98\x82"_q,
u"\xf0\x9f\x98\xae"_q,
u"\xf0\x9f\x99\x84"_q,
u"\xf0\x9f\x98\x8e"_q,
u"\xf0\x9f\x91\x8e"_q,
};
}
rpl::producer<std::vector<GifSection>> GifSectionsValue(
not_null<Main::Session*> session) {
const auto config = &session->account().appConfig();
return rpl::single(
rpl::empty_value()
) | rpl::then(
config->refreshed()
) | rpl::map([=] {
return config->get<std::vector<QString>>(
u"gif_search_emojies"_q,
GifSearchEmojiFallback());
}) | rpl::distinct_until_changed(
) | rpl::map([=](const std::vector<QString> &emoji) {
const auto list = ranges::views::all(
emoji
) | ranges::views::transform([](const QString &val) {
return Ui::Emoji::Find(val);
}) | ranges::views::filter([](EmojiPtr emoji) {
return emoji != nullptr;
}) | ranges::to_vector;
const auto pack = &session->emojiStickersPack();
return rpl::single(
rpl::empty_value()
) | rpl::then(
pack->refreshed()
) | rpl::map([=, list = std::move(list)] {
return list | ranges::views::transform([&](EmojiPtr emoji) {
const auto document = pack->stickerForEmoji(emoji).document;
return GifSection{ document, emoji };
}) | ranges::views::filter([](GifSection section) {
return (section.document != nullptr);
}) | ranges::to_vector;
}) | rpl::distinct_until_changed();
}) | rpl::flatten_latest();
}
[[nodiscard]] std::vector<EmojiPtr> SearchEmoji(
const std::vector<QString> &query,
base::flat_set<EmojiPtr> &outResultSet) {
auto result = std::vector<EmojiPtr>();
const auto pushPlain = [&](EmojiPtr emoji) {
if (result.size() < kEmojiSearchLimit
&& outResultSet.emplace(emoji).second) {
result.push_back(emoji);
}
if (const auto original = emoji->original(); original != emoji) {
outResultSet.emplace(original);
}
};
auto refreshed = false;
auto &keywords = Core::App().emojiKeywords();
for (const auto &entry : query) {
if (const auto emoji = Ui::Emoji::Find(entry)) {
pushPlain(emoji);
if (result.size() >= kEmojiSearchLimit) {
return result;
}
} else if (!entry.isEmpty()) {
if (!refreshed) {
refreshed = true;
keywords.refresh();
}
const auto list = keywords.queryMine(entry);
for (const auto &entry : list) {
pushPlain(entry.emoji);
if (result.size() >= kEmojiSearchLimit) {
return result;
}
}
}
}
return result;
}
StickerIcon::StickerIcon(uint64 setId) : setId(setId) {
}
StickerIcon::StickerIcon(
not_null<Data::StickersSet*> set,
DocumentData *sticker,
int pixw,
int pixh)
: setId(set->id)
, set(set)
, sticker(sticker)
, pixw(std::max(pixw, 1))
, pixh(std::max(pixh, 1)) {
}
StickerIcon::StickerIcon(StickerIcon&&) = default;
StickerIcon &StickerIcon::operator=(StickerIcon&&) = default;
StickerIcon::~StickerIcon() = default;
void StickerIcon::ensureMediaCreated() const {
if (!sticker) {
return;
} else if (set->hasThumbnail()) {
if (!thumbnailMedia) {
thumbnailMedia = set->createThumbnailView();
set->loadThumbnail();
}
} else if (!stickerMedia) {
stickerMedia = sticker->createMediaView();
stickerMedia->thumbnailWanted(sticker->stickerSetOrigin());
}
}
template <typename UpdateCallback>
StickersListFooter::ScrollState::ScrollState(UpdateCallback &&callback)
: animation([=](crl::time now) {
callback();
return animationCallback(now);
}) {
}
bool StickersListFooter::ScrollState::animationCallback(crl::time now) {
if (anim::Disabled()) {
now += st::stickerIconMove;
}
if (!animationStart) {
return false;
}
const auto dt = (now - animationStart) / float64(st::stickerIconMove);
if (dt >= 1.) {
animationStart = 0;
x.finish();
selectionX.finish();
selectionWidth.finish();
return false;
}
x.update(dt, anim::linear);
selectionX.update(dt, anim::easeOutCubic);
selectionWidth.update(dt, anim::easeOutCubic);
return true;
}
GradientPremiumStar::GradientPremiumStar() {
style::PaletteChanged(
) | rpl::start_with_next([=] {
_image = QImage();
}, _lifetime);
}
QImage GradientPremiumStar::image() const {
if (_image.isNull()) {
renderOnDemand();
}
return _image;
}
void GradientPremiumStar::renderOnDemand() const {
const auto size = st::emojiStatusDefault.size();
const auto mask = st::emojiStatusDefault.instance(Qt::white);
const auto factor = style::DevicePixelRatio();
_image = QImage(
size * factor,
QImage::Format_ARGB32_Premultiplied);
_image.setDevicePixelRatio(factor);
QPainter p(&_image);
auto gradient = QLinearGradient(
QPoint(0, size.height()),
QPoint(size.width(), 0));
gradient.setStops({
{ 0., st::stickerPanPremium1->c },
{ 1., st::stickerPanPremium2->c },
});
p.fillRect(QRect(QPoint(), size), gradient);
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
p.drawImage(QRect(QPoint(), size), mask);
}
StickersListFooter::StickersListFooter(Descriptor &&descriptor)
: InnerFooter(
descriptor.parent,
descriptor.st ? *descriptor.st : st::defaultEmojiPan)
, _session(descriptor.session)
, _customTextColor(std::move(descriptor.customTextColor))
, _paused(std::move(descriptor.paused))
, _features(descriptor.features)
, _iconState([=] { update(); })
, _subiconState([=] { update(); })
, _selectionBg(st::emojiPanRadius, st().categoriesBgOver)
, _subselectionBg(st().iconArea / 2, st().categoriesBgOver)
, _forceFirstFrame(descriptor.forceFirstFrame) {
setMouseTracking(true);
_iconsLeft = st().iconSkip
+ (_features.stickersSettings ? st().iconWidth : 0);
_iconsRight = st().iconSkip;
_session->downloaderTaskFinished(
) | rpl::start_with_next([=] {
update();
}, lifetime());
}
void StickersListFooter::clearHeavyData() {
enumerateIcons([&](const IconInfo &info) {
auto &icon = _icons[info.index];
icon.webm = nullptr;
icon.lottie = nullptr;
icon.lifetime.destroy();
icon.stickerMedia = nullptr;
if (!info.visible) {
icon.savedFrame = QImage();
}
return true;
});
}
void StickersListFooter::paintExpanding(
Painter &p,
QRect clip,
float64 radius,
RectPart origin) {
const auto delta = ((origin | RectPart::None) & RectPart::FullBottom)
? (height() - clip.height())
: 0;
const auto shift = QPoint(clip.x(), clip.y() - delta);
p.translate(shift);
const auto context = ExpandingContext{
.clip = clip.translated(-shift),
.progress = clip.height() / float64(height()),
.radius = int(std::ceil(radius)),
.expanding = true,
};
paint(p, context);
p.translate(-shift);
p.setClipping(false);
}
int StickersListFooter::IconFrameSize() {
return Data::FrameSizeFromTag(
Data::CustomEmojiManager::SizeTag::SetIcon
) / style::DevicePixelRatio();
}
void StickersListFooter::enumerateVisibleIcons(
Fn<void(const IconInfo &)> callback) const {
enumerateIcons([&](const IconInfo &info) {
if (info.visible) {
callback(info);
} else if (info.adjustedLeft > 0) {
return false;
}
return true;
});
}
void StickersListFooter::enumerateIcons(
Fn<bool(const IconInfo &)> callback) const {
auto left = 0;
const auto iconsX = int(base::SafeRound(_iconState.x.current()));
const auto shift = _iconsLeft - iconsX;
const auto emojiId = AllEmojiSectionSetId();
const auto right = width();
for (auto i = 0, count = int(_icons.size()); i != count; ++i) {
auto &icon = _icons[i];
const auto width = (icon.setId == emojiId)
? _subiconsWidthAnimation.value(_subiconsExpanded
? _subiconsWidth
: _singleWidth)
: _singleWidth;
const auto shifted = shift + left;
const auto visible = (shifted + width > 0 && shifted < right);
const auto result = callback({
.index = i,
.left = left,
.adjustedLeft = shifted,
.width = int(base::SafeRound(width)),
.visible = visible,
});
if (!result) {
break;
}
left += width;
}
}
void StickersListFooter::enumerateSubicons(
Fn<bool(const IconInfo &)> callback) const {
auto left = 0;
const auto iconsX = int(base::SafeRound(_subiconState.x.current()));
const auto shift = -iconsX;
const auto right = _subiconsWidth;
using Section = Ui::Emoji::Section;
for (auto i = int(Section::People); i <= int(Section::Symbols); ++i) {
const auto shifted = shift + left;
const auto visible = (shifted + _singleWidth > 0 && shifted < right);
const auto result = callback({
.index = i - int(Section::People),
.left = left,
.adjustedLeft = shifted,
.width = _singleWidth,
.visible = visible,
});
if (!result) {
break;
}
left += _singleWidth;
}
}
auto StickersListFooter::iconInfo(int index) const -> IconInfo {
if (index < 0) {
const auto iconsX = int(base::SafeRound(_iconState.x.current()));
return {
.index = -1,
.left = -_singleWidth - _iconsLeft,
.adjustedLeft = -_singleWidth - _iconsLeft - iconsX,
.width = _singleWidth,
.visible = false,
};
}
auto result = IconInfo();
enumerateIcons([&](const IconInfo &info) {
if (info.index == index) {
result = info;
return false;
}
return true;
});
return result;
}
auto StickersListFooter::subiconInfo(int index) const -> IconInfo {
auto result = IconInfo();
enumerateSubicons([&](const IconInfo &info) {
if (info.index == index) {
result = info;
return false;
}
return true;
});
return result;
}
void StickersListFooter::preloadImages() {
enumerateVisibleIcons([&](const IconInfo &info) {
const auto &icon = _icons[info.index];
if (const auto sticker = icon.sticker) {
Assert(icon.set != nullptr);
if (icon.set->hasThumbnail()) {
icon.set->loadThumbnail();
} else {
sticker->loadThumbnail(sticker->stickerSetOrigin());
}
}
});
}
void StickersListFooter::validateSelectedIcon(
uint64 setId,
ValidateIconAnimations animations) {
_activeByScrollId = setId;
using EmojiSection = Ui::Emoji::Section;
auto favedIconIndex = -1;
auto newSelected = -1;
auto newSubSelected = -1;
const auto emojiSection = SetIdEmojiSection(setId);
const auto isEmojiSection = emojiSection.has_value()
&& (emojiSection != EmojiSection::Recent);
const auto allEmojiSetId = AllEmojiSectionSetId();
for (auto i = 0, l = int(_icons.size()); i != l; ++i) {
if (_icons[i].setId == setId
|| (_icons[i].setId == Data::Stickers::FavedSetId
&& setId == Data::Stickers::RecentSetId)) {
newSelected = i;
break;
} else if (_icons[i].setId == Data::Stickers::FavedSetId
&& setId != SearchEmojiSectionSetId()) {
favedIconIndex = i;
} else if (isEmojiSection && _icons[i].setId == allEmojiSetId) {
newSelected = i;
newSubSelected = setId - EmojiSectionSetId(EmojiSection::People);
}
}
setSelectedIcon(
(newSelected >= 0
? newSelected
: (favedIconIndex >= 0)
? favedIconIndex
: -1),
animations);
setSelectedSubicon(
(newSubSelected >= 0 ? newSubSelected : 0),
animations);
}
void StickersListFooter::updateEmojiSectionWidth() {
const auto expanded = (_iconState.selected >= 0)
&& (_iconState.selected < _icons.size())
&& (_icons[_iconState.selected].setId == AllEmojiSectionSetId());
if (_subiconsExpanded == expanded) {
return;
}
_subiconsExpanded = expanded;
_subiconsWidthAnimation.start(
[=] { updateEmojiWidthCallback(); },
expanded ? _singleWidth : _subiconsWidth,
expanded ? _subiconsWidth : _singleWidth,
st::slideDuration);
}
void StickersListFooter::updateEmojiWidthCallback() {
refreshScrollableDimensions();
const auto info = iconInfo(_iconState.selected);
UpdateAnimated(_iconState.selectionX, info.left);
UpdateAnimated(_iconState.selectionWidth, info.width);
if (_iconState.animation.animating()) {
_iconState.animationCallback(crl::now());
}
update();
}
void StickersListFooter::setSelectedIcon(
int newSelected,
ValidateIconAnimations animations) {
if (_iconState.selected == newSelected) {
return;
}
if ((_iconState.selected < 0) != (newSelected < 0)) {
animations = ValidateIconAnimations::None;
}
_iconState.selected = newSelected;
updateEmojiSectionWidth();
const auto info = iconInfo(_iconState.selected);
UpdateAnimated(_iconState.selectionX, info.left, animations);
UpdateAnimated(_iconState.selectionWidth, info.width, animations);
const auto relativeLeft = info.left - _iconsLeft;
const auto iconsWidthForCentering = 2 * relativeLeft + info.width;
const auto iconsXFinal = std::clamp(
(_iconsLeft + iconsWidthForCentering + _iconsRight - width()) / 2,
0,
_iconState.max);
if (animations == ValidateIconAnimations::None) {
_iconState.x = anim::value(iconsXFinal, iconsXFinal);
_iconState.animation.stop();
} else {
_iconState.x.start(iconsXFinal);
_iconState.animationStart = crl::now();
_iconState.animation.start();
}
updateSelected();
update();
}
void StickersListFooter::setSelectedSubicon(
int newSelected,
ValidateIconAnimations animations) {
if (_subiconState.selected == newSelected) {
return;
}
_subiconState.selected = newSelected;
const auto info = subiconInfo(_subiconState.selected);
const auto relativeLeft = info.left;
const auto subiconsWidthForCentering = 2 * relativeLeft + info.width;
const auto subiconsXFinal = std::clamp(
(subiconsWidthForCentering - _subiconsWidth) / 2,
0,
_subiconState.max);
if (animations == ValidateIconAnimations::None) {
_subiconState.x = anim::value(subiconsXFinal, subiconsXFinal);
_subiconState.animation.stop();
} else {
_subiconState.x.start(subiconsXFinal);
_subiconState.animationStart = crl::now();
_subiconState.animation.start();
}
updateSelected();
update();
}
void StickersListFooter::processHideFinished() {
_selected = _pressed = SpecialOver::None;
_iconState.animation.stop();
_iconState.animationStart = 0;
_iconState.x.finish();
_iconState.selectionX.finish();
_iconState.selectionWidth.finish();
_subiconState.animation.stop();
_subiconState.animationStart = 0;
_subiconState.x.finish();
}
void StickersListFooter::leaveToChildEvent(QEvent *e, QWidget *child) {
_iconsMousePos = QCursor::pos();
updateSelected();
}
void StickersListFooter::paintEvent(QPaintEvent *e) {
auto p = Painter(this);
_repaintScheduled = false;
paint(p, {});
}
void StickersListFooter::paint(
Painter &p,
const ExpandingContext &context) const {
if (_icons.empty()) {
return;
}
if (_features.stickersSettings) {
paintStickerSettingsIcon(p);
}
auto clip = QRect(
_iconsLeft,
_iconsTop,
width() - _iconsLeft - _iconsRight,
st().footer);
if (rtl()) {
clip.moveLeft(width() - _iconsLeft - clip.width());
}
if (context.expanding) {
const auto both = clip.intersected(
context.clip.marginsRemoved(
{ context.radius, 0, context.radius, 0 }));
if (both.isEmpty()) {
return;
}
p.setClipRect(both);
} else {
p.setClipRect(clip);
}
paintSelectionBg(p, context);
const auto iconCacheSize = QSize(_singleWidth, st().footer);
const auto full = iconCacheSize * style::DevicePixelRatio();
if (_setIconCache.size() != full) {
_setIconCache = QImage(full, QImage::Format_ARGB32_Premultiplied);
_setIconCache.setDevicePixelRatio(style::DevicePixelRatio());
}
const auto now = crl::now();
const auto paused = _paused();
p.setPen(st::windowFg);
enumerateVisibleIcons([&](const IconInfo &info) {
paintSetIcon(p, context, info, now, paused);
});
paintLeftRightFading(p, context);
}
void StickersListFooter::paintSelectionBg(
QPainter &p,
const ExpandingContext &context) const {
auto selxrel = _iconsLeft + qRound(_iconState.selectionX.current());
auto selx = selxrel - qRound(_iconState.x.current());
const auto selw = qRound(_iconState.selectionWidth.current());
if (rtl()) {
selx = width() - selx - selw;
}
const auto sely = _iconsTop;
const auto area = st().iconArea;
auto rect = QRect(
QPoint(selx, sely) + _areaPosition,
QSize(selw - 2 * _areaPosition.x(), area));
if (context.expanding) {
const auto recthalf = rect.height() / 2;
const auto myhalf = height() / 2;
const auto sub = anim::interpolate(recthalf, 0, context.progress);
const auto shift = anim::interpolate(myhalf, 0, context.progress);
rect = rect.marginsRemoved(
{ sub, sub, sub, sub }
).translated(0, shift);
}
if (rect.width() == rect.height() || _subiconsWidth <= _singleWidth) {
_selectionBg.paint(p, rect);
} else if (selw == _subiconsWidth) {
_subselectionBg.paint(p, rect);
} else {
PainterHighQualityEnabler hq(p);
const auto progress = (selw - _singleWidth)
/ float64(_subiconsWidth - _singleWidth);
const auto radius = anim::interpolate(
st::roundRadiusLarge,
area / 2,
progress);
p.setPen(Qt::NoPen);
p.setBrush(st().categoriesBgOver);
p.drawRoundedRect(rect, radius, radius);
}
}
void StickersListFooter::paintLeftRightFading(
QPainter &p,
const ExpandingContext &context) const {
const auto o_left_normal = std::clamp(
_iconState.x.current() / st().fadeLeft.width(),
0.,
1.);
const auto o_left = context.expanding
? (1. - context.progress * (1. - o_left_normal))
: o_left_normal;
const auto radiusSkip = context.expanding
? std::max(context.radius - st::emojiPanRadius, 0)
: 0;
if (o_left > 0) {
p.setOpacity(o_left);
const auto left = std::max(_iconsLeft, radiusSkip);
const auto top = _iconsTop;
if (left >= st::emojiPanRadius) {
st().fadeLeft.fill(
p,
QRect(left, top, st().fadeLeft.width(), st().footer));
} else {
validateFadeLeft(left + st().fadeLeft.width());
p.drawImage(0, _iconsTop, _fadeLeftCache);
}
p.setOpacity(1.);
}
const auto o_right_normal = std::clamp(
(_iconState.max - _iconState.x.current()) / st().fadeRight.width(),
0.,
1.);
const auto o_right = context.expanding
? (1. - context.progress * (1. - o_right_normal))
: o_right_normal;
if (o_right > 0) {
p.setOpacity(o_right);
const auto right = std::max(_iconsRight, radiusSkip);
const auto rightWidth = right + st().fadeRight.width();
if (right >= st::emojiPanRadius) {
st().fadeRight.fill(
p,
QRect(
width() - rightWidth,
_iconsTop,
st().fadeRight.width(),
st().footer));
} else {
validateFadeRight(rightWidth);
p.drawImage(width() - rightWidth, _iconsTop, _fadeRightCache);
}
p.setOpacity(1.);
}
}
void StickersListFooter::validateFadeLeft(int leftWidth) const {
validateFadeMask();
const auto ratio = devicePixelRatioF();
const auto &color = st().categoriesBg->c;
dpr::Validate(_fadeLeftCache, ratio, { leftWidth, st().footer }, [&](
QPainter &p,
QSize size) {
_fadeLeftColor = color;
const auto frame = dpr::IconFrame(st().fadeLeft, color, ratio);
p.drawImage(
QRect(
size.width() - frame.width(),
0,
frame.width(),
size.height()),
frame);
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
p.drawImage(0, 0, _fadeMask);
}, (_fadeLeftColor != color), Qt::transparent);
}
void StickersListFooter::validateFadeRight(int rightWidth) const {
validateFadeMask();
const auto ratio = devicePixelRatioF();
const auto &color = st().categoriesBg->c;
dpr::Validate(_fadeRightCache, ratio, { rightWidth, st().footer }, [&](
QPainter &p,
QSize size) {
_fadeRightColor = color;
const auto frame = dpr::IconFrame(st().fadeRight, color, ratio);
p.drawImage(QRect(0, 0, frame.width(), size.height()), frame);
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
p.drawImage(size.width() - _fadeMask.width(), 0, _fadeMask);
}, (_fadeRightColor != color), Qt::transparent);
}
void StickersListFooter::validateFadeMask() const {
const auto ratio = devicePixelRatioF();
const auto width = st().fadeLeft.width()
+ st().fadeRight.width()
+ 2 * st::emojiPanRadius;
dpr::Validate(_fadeMask, ratio, { width, st().footer }, [&](
QPainter &p,
QSize size) {
const auto radius = st::emojiPanRadius * ratio;
p.setBrush(Qt::white);
p.setPen(Qt::NoPen);
auto hq = PainterHighQualityEnabler(p);
p.drawRoundedRect(QRect(QPoint(), size), radius, radius);
}, false, Qt::transparent, false);
}
void StickersListFooter::resizeEvent(QResizeEvent *e) {
refreshIconsGeometry(_activeByScrollId, ValidateIconAnimations::None);
}
rpl::producer<uint64> StickersListFooter::setChosen() const {
return _setChosen.events();
}
rpl::producer<> StickersListFooter::openSettingsRequests() const {
return _openSettingsRequests.events();
}
void StickersListFooter::mousePressEvent(QMouseEvent *e) {
if (e->button() != Qt::LeftButton) {
return;
}
_iconsMousePos = e ? e->globalPos() : QCursor::pos();
updateSelected();
if (_selected == SpecialOver::Settings) {
_openSettingsRequests.fire({});
} else {
_pressed = _selected;
_iconsMouseDown = _iconsMousePos;
_iconState.draggingStartX = qRound(_iconState.x.current());
_subiconState.draggingStartX = qRound(_subiconState.x.current());
}
}
void StickersListFooter::mouseMoveEvent(QMouseEvent *e) {
_iconsMousePos = e ? e->globalPos() : QCursor::pos();
updateSelected();
if (!_iconState.dragging
&& !_icons.empty()
&& v::is<IconId>(_pressed)) {
if ((_iconsMousePos - _iconsMouseDown).manhattanLength() >= QApplication::startDragDistance()) {
const auto &icon = _icons[v::get<IconId>(_pressed).index];
(icon.setId == AllEmojiSectionSetId()
? _subiconState
: _iconState).dragging = true;
}
}
checkDragging(_iconState);
checkDragging(_subiconState);
}
void StickersListFooter::checkDragging(ScrollState &state) {
if (state.dragging) {
const auto newX = std::clamp(
(rtl() ? -1 : 1) * (_iconsMouseDown.x() - _iconsMousePos.x())
+ state.draggingStartX,
0,
state.max);
if (newX != qRound(state.x.current())) {
state.x = anim::value(newX, newX);
state.animationStart = 0;
state.animation.stop();
update();
}
}
}
void StickersListFooter::mouseReleaseEvent(QMouseEvent *e) {
if (_icons.empty()) {
return;
}
const auto wasDown = std::exchange(_pressed, SpecialOver::None);
_iconsMousePos = e ? e->globalPos() : QCursor::pos();
if (finishDragging()) {
return;
}
updateSelected();
if (wasDown == _selected) {
if (const auto icon = std::get_if<IconId>(&_selected)) {
const auto info = iconInfo(icon->index);
_iconState.selectionX = anim::value(info.left, info.left);
_iconState.selectionWidth = anim::value(info.width, info.width);
const auto setId = _icons[icon->index].setId;
_setChosen.fire_copy((setId == AllEmojiSectionSetId())
? EmojiSectionSetId(
EmojiSection(int(EmojiSection::People) + icon->subindex))
: setId);
}
}
}
bool StickersListFooter::finishDragging() {
const auto icon = finishDragging(_iconState);
const auto subicon = finishDragging(_subiconState);
return icon || subicon;
}
bool StickersListFooter::finishDragging(ScrollState &state) {
if (!state.dragging) {
return false;
}
const auto newX = std::clamp(
state.draggingStartX + _iconsMouseDown.x() - _iconsMousePos.x(),
0,
state.max);
if (newX != qRound(state.x.current())) {
state.x = anim::value(newX, newX);
state.animationStart = 0;
state.animation.stop();
update();
}
state.dragging = false;
updateSelected();
return true;
}
bool StickersListFooter::eventHook(QEvent *e) {
if (e->type() == QEvent::TouchBegin) {
} else if (e->type() == QEvent::Wheel) {
if (!_icons.empty()
&& v::is<IconId>(_selected)
&& (_pressed == SpecialOver::None)) {
scrollByWheelEvent(static_cast<QWheelEvent*>(e));
}
}
return InnerFooter::eventHook(e);
}
void StickersListFooter::scrollByWheelEvent(
not_null<QWheelEvent*> e) {
auto horizontal = (e->angleDelta().x() != 0);
auto vertical = (e->angleDelta().y() != 0);
if (!horizontal && !vertical) {
return;
}
auto delta = horizontal
? ((rtl() ? -1 : 1) * (e->pixelDelta().x()
? e->pixelDelta().x()
: e->angleDelta().x()))
: (e->pixelDelta().y()
? e->pixelDelta().y()
: e->angleDelta().y());
const auto use = [&](ScrollState &state) {
const auto now = qRound(state.x.current());
const auto used = now - delta;
const auto next = std::clamp(used, 0, state.max);
delta = next - used;
if (next != now) {
state.x = anim::value(next, next);
state.animationStart = 0;
state.animation.stop();
updateSelected();
update();
}
};
const auto index = v::get<IconId>(_selected).index;
if (_subiconsExpanded
&& _icons[index].setId == AllEmojiSectionSetId()) {
use(_subiconState);
} else {
use(_iconState);
}
}
void StickersListFooter::clipCallback(
Media::Clip::Notification notification,
uint64 setId) {
using namespace Media::Clip;
switch (notification) {
case Notification::Reinit: {
enumerateIcons([&](const IconInfo &info) {
auto &icon = _icons[info.index];
if (icon.setId != setId || !icon.webm) {
return true;
} else if (icon.webm->state() == State::Error) {
icon.webm.setBad();
} else if (!info.visible) {
icon.webm = nullptr;
} else if (icon.webm->ready() && !icon.webm->started()) {
icon.webm->start({
.frame = { icon.pixw, icon.pixh },
.keepAlpha = true,
});
}
updateSetIconAt(info.adjustedLeft);
return true;
});
} break;
case Notification::Repaint:
updateSetIcon(setId);
break;
}
}
void StickersListFooter::updateSelected() {
if (_pressed != SpecialOver::None) {
return;
}
auto p = mapFromGlobal(_iconsMousePos);
auto x = p.x(), y = p.y();
if (rtl()) x = width() - x;
const auto settingsLeft = _iconsLeft - _singleWidth;
auto newOver = OverState(SpecialOver::None);
if (_features.stickersSettings
&& x >= settingsLeft
&& x < settingsLeft + _singleWidth
&& y >= _iconsTop
&& y < _iconsTop + st().footer) {
if (!_icons.empty()) {
newOver = SpecialOver::Settings;
}
} else if (!_icons.empty()) {
if (y >= _iconsTop
&& y < _iconsTop + st().footer
&& x >= _iconsLeft
&& x < width() - _iconsRight) {
enumerateIcons([&](const IconInfo &info) {
if (x >= info.adjustedLeft
&& x < info.adjustedLeft + info.width) {
newOver = IconId{ .index = info.index };
if (_icons[info.index].setId == AllEmojiSectionSetId()) {
const auto subx = (x - info.adjustedLeft);
enumerateSubicons([&](const IconInfo &info) {
if (subx >= info.adjustedLeft
&& subx < info.adjustedLeft + info.width) {
v::get<IconId>(newOver).subindex = info.index;
return false;
}
return true;
});
}
return false;
}
return true;
});
}
}
if (newOver != _selected) {
if (newOver == SpecialOver::None) {
setCursor(style::cur_default);
} else if (_selected == SpecialOver::None) {
setCursor(style::cur_pointer);
}
_selected = newOver;
}
}
auto StickersListFooter::getLottieRenderer()
-> std::shared_ptr<Lottie::FrameRenderer> {
if (auto result = _lottieRenderer.lock()) {
return result;
}
auto result = Lottie::MakeFrameRenderer();
_lottieRenderer = result;
return result;
}
void StickersListFooter::refreshIcons(
std::vector<StickerIcon> icons,
uint64 activeSetId,
Fn<std::shared_ptr<Lottie::FrameRenderer>()> renderer,
ValidateIconAnimations animations) {
_renderer = renderer
? std::move(renderer)
: [=] { return getLottieRenderer(); };
auto indices = base::flat_map<uint64, int>();
indices.reserve(_icons.size());
auto index = 0;
for (const auto &entry : _icons) {
indices.emplace(entry.setId, index++);
}
for (auto &now : icons) {
if (const auto i = indices.find(now.setId); i != end(indices)) {
auto &was = _icons[i->second];
if (now.sticker == was.sticker) {
now.webm = std::move(was.webm);
now.lottie = std::move(was.lottie);
now.custom = std::move(was.custom);
now.lifetime = std::move(was.lifetime);
now.savedFrame = std::move(was.savedFrame);
}
}
}
_icons = std::move(icons);
refreshIconsGeometry(activeSetId, animations);
}
void StickersListFooter::refreshScrollableDimensions() {
const auto &last = iconInfo(_icons.size() - 1);
_iconState.max = std::max(
last.left + last.width + _iconsLeft + _iconsRight - width(),
0);
if (_iconState.x.current() > _iconState.max) {
_iconState.x = anim::value(_iconState.max, _iconState.max);
}
}
void StickersListFooter::refreshIconsGeometry(
uint64 activeSetId,
ValidateIconAnimations animations) {
_selected = _pressed = SpecialOver::None;
_iconState.x.finish();
_iconState.selectionX.finish();
_iconState.selectionWidth.finish();
_iconState.animationStart = 0;
_iconState.animation.stop();
if (_icons.size() > 1
&& _icons[1].setId == EmojiSectionSetId(EmojiSection::People)) {
_singleWidth = (width() - _iconsLeft - _iconsRight) / _icons.size();
} else {
_singleWidth = st().iconWidth;
}
_areaPosition = QPoint(
(_singleWidth - st().iconArea) / 2,
(st().footer - st().iconArea) / 2);
refreshScrollableDimensions();
refreshSubiconsGeometry();
_iconState.selected = _subiconState.selected = -2;
validateSelectedIcon(activeSetId, animations);
update();
}
void StickersListFooter::refreshSubiconsGeometry() {
using Section = Ui::Emoji::Section;
_subiconState.x.finish();
_subiconState.animationStart = 0;
_subiconState.animation.stop();
const auto half = _singleWidth / 2;
const auto count = int(Section::Symbols) - int(Section::Recent);
const auto widthMax = count * _singleWidth;
const auto widthMin = 5 * _singleWidth + half;
const auto collapsedWidth = int(_icons.size()) * _singleWidth;
_subiconsWidth = std::clamp(
width() + _singleWidth - collapsedWidth,
widthMin,
widthMax);
if (_subiconsWidth < widthMax) {
_subiconsWidth = half
+ (((_subiconsWidth - half) / _singleWidth) * _singleWidth);
}
_subiconState.max = std::max(
widthMax - _subiconsWidth,
0);
if (_subiconState.x.current() > _subiconState.max) {
_subiconState.x = anim::value(_subiconState.max, _subiconState.max);
}
updateEmojiWidthCallback();
}
void StickersListFooter::paintStickerSettingsIcon(QPainter &p) const {
const auto settingsLeft = _iconsLeft - _singleWidth;
st().icons.settings.paint(
p,
(settingsLeft + (_singleWidth - st().icons.settings.width()) / 2),
_iconsTop + st::emojiCategoryIconTop,
width());
}
void StickersListFooter::customEmojiRepaint() {
if (!_repaintScheduled) {
_repaintScheduled = true;
update();
}
}
void StickersListFooter::validateIconLottieAnimation(
const StickerIcon &icon) {
icon.ensureMediaCreated();
if (icon.lottie
|| !icon.sticker
|| !HasLottieThumbnail(
icon.set ? icon.set->flags : Data::StickersSetFlags(),
icon.thumbnailMedia.get(),
icon.stickerMedia.get())) {
return;
}
auto player = LottieThumbnail(
icon.thumbnailMedia.get(),
icon.stickerMedia.get(),
StickerLottieSize::StickersFooter,
QSize(icon.pixw, icon.pixh) * cIntRetinaFactor(),
_renderer());
if (!player) {
return;
}
icon.lottie = std::move(player);
const auto id = icon.setId;
icon.lottie->updates(
) | rpl::start_with_next([=] {
updateSetIcon(id);
}, icon.lifetime);
}
void StickersListFooter::validateIconWebmAnimation(
const StickerIcon &icon) {
icon.ensureMediaCreated();
if (icon.webm
|| !icon.sticker
|| !HasWebmThumbnail(
icon.set ? icon.set->flags : Data::StickersSetFlags(),
icon.thumbnailMedia.get(),
icon.stickerMedia.get())) {
return;
}
const auto id = icon.setId;
auto callback = [=](Media::Clip::Notification notification) {
clipCallback(notification, id);
};
icon.webm = WebmThumbnail(
icon.thumbnailMedia.get(),
icon.stickerMedia.get(),
std::move(callback));
}
void StickersListFooter::validateIconAnimation(
const StickerIcon &icon) {
const auto emoji = icon.sticker;
if (emoji && emoji->sticker()->setType == Data::StickersType::Emoji) {
if (!icon.custom) {
const auto tag = Data::CustomEmojiManager::SizeTag::SetIcon;
auto &manager = emoji->owner().customEmojiManager();
icon.custom = manager.create(
emoji->id,
[=] { customEmojiRepaint(); },
tag);
}
return;
}
validateIconWebmAnimation(icon);
validateIconLottieAnimation(icon);
}
void StickersListFooter::updateSetIcon(uint64 setId) {
enumerateVisibleIcons([&](const IconInfo &info) {
if (_icons[info.index].setId != setId) {
return;
}
updateSetIconAt(info.adjustedLeft);
});
}
void StickersListFooter::updateSetIconAt(int left) {
update(left, _iconsTop, _singleWidth, st().footer);
}
void StickersListFooter::paintSetIcon(
Painter &p,
const ExpandingContext &context,
const IconInfo &info,
crl::time now,
bool paused) const {
const auto &icon = _icons[info.index];
const auto expandingShift = context.expanding
? QPoint(
0,
anim::interpolate(height() / 2, 0, context.progress))
: QPoint();
if (icon.sticker) {
icon.ensureMediaCreated();
const_cast<StickersListFooter*>(this)->validateIconAnimation(icon);
}
if (context.expanding) {
if (icon.custom) {
p.translate(expandingShift);
} else {
p.save();
const auto center = QPoint(
info.adjustedLeft + _singleWidth / 2,
_iconsTop + st().footer / 2);
p.translate(expandingShift + center);
p.scale(context.progress, context.progress);
p.translate(-center);
}
}
if (icon.sticker) {
prepareSetIcon(context, info, now, paused);
p.drawImage(info.adjustedLeft, _iconsTop, _setIconCache);
} else {
p.translate(info.adjustedLeft, _iconsTop);
paintSetIconToCache(p, context, info, now, paused);
p.translate(-info.adjustedLeft, -_iconsTop);
}
if (context.expanding) {
if (icon.custom) {
p.translate(-expandingShift);
} else {
p.restore();
}
}
}
void StickersListFooter::prepareSetIcon(
const ExpandingContext &context,
const IconInfo &info,
crl::time now,
bool paused) const {
_setIconCache.fill(Qt::transparent);
auto p = Painter(&_setIconCache);
paintSetIconToCache(p, context, info, now, paused);
if (!_icons[info.index].sticker) {
return;
}
// Rounding the corners.
auto hq = PainterHighQualityEnabler(p);
p.setCompositionMode(QPainter::CompositionMode_Source);
p.setBrush(Qt::NoBrush);
auto pen = QPen(Qt::transparent);
pen.setWidth(style::ConvertScaleExact(4.));
p.setPen(pen);
const auto area = st().iconArea;
auto rect = QRect(_areaPosition, QSize(area, area));
p.drawRoundedRect(rect, st::emojiPanRadius, st::emojiPanRadius);
}
void StickersListFooter::paintSetIconToCache(
Painter &p,
const ExpandingContext &context,
const IconInfo &info,
crl::time now,
bool paused) const {
const auto &icon = _icons[info.index];
if (icon.sticker) {
const auto origin = icon.sticker->stickerSetOrigin();
const auto thumb = icon.thumbnailMedia
? icon.thumbnailMedia->image()
: icon.stickerMedia
? icon.stickerMedia->thumbnail()
: nullptr;
const auto x = (_singleWidth - icon.pixw) / 2;
const auto y = (st().footer - icon.pixh) / 2;
if (icon.custom) {
icon.custom->paint(p, Ui::Text::CustomEmoji::Context{
.textColor = (_customTextColor
? _customTextColor()
: st().textFg->c),
.size = QSize(icon.pixw, icon.pixh),
.now = now,
.scale = context.progress,
.position = { x, y },
.paused = paused,
.scaled = context.expanding,
.internal = { .forceFirstFrame = _forceFirstFrame },
});
} else if (icon.lottie && icon.lottie->ready()) {
const auto frame = icon.lottie->frame();
const auto size = frame.size() / cIntRetinaFactor();
if (icon.savedFrame.isNull()) {
icon.savedFrame = frame;
icon.savedFrame.setDevicePixelRatio(cRetinaFactor());
}
p.drawImage(
QRect(
(_singleWidth - size.width()) / 2,
(st().footer - size.height()) / 2,
size.width(),
size.height()),
frame);
if (!paused) {
icon.lottie->markFrameShown();
}
} else if (icon.webm && icon.webm->started()) {
const auto frame = icon.webm->current(
{ .frame = { icon.pixw, icon.pixh }, .keepAlpha = true },
paused ? 0 : now);
if (icon.savedFrame.isNull()) {
icon.savedFrame = frame;
icon.savedFrame.setDevicePixelRatio(cRetinaFactor());
}
p.drawImage(x, y, frame);
} else if (!icon.savedFrame.isNull()) {
p.drawImage(x, y, icon.savedFrame);
} else if (thumb) {
const auto pixmap = (!icon.lottie && thumb)
? thumb->pix(icon.pixw, icon.pixh)
: QPixmap();
if (pixmap.isNull()) {
return;
} else if (icon.savedFrame.isNull()) {
icon.savedFrame = pixmap.toImage();
}
p.drawPixmapLeft(x, y, width(), pixmap);
}
} else if (icon.megagroup) {
const auto size = st::stickerGroupCategorySize;
icon.megagroup->paintUserpicLeft(
p,
icon.megagroupUserpic,
(_singleWidth - size) / 2,
(st().footer - size) / 2,
width(),
st::stickerGroupCategorySize);
} else {
using Section = Ui::Emoji::Section;
const auto sectionIcon = [&](Section section, bool active) {
const auto icons = std::array{
&st().icons.recent,
&st().icons.recentActive,
&st().icons.people,
&st().icons.peopleActive,
&st().icons.nature,
&st().icons.natureActive,
&st().icons.food,
&st().icons.foodActive,
&st().icons.activity,
&st().icons.activityActive,
&st().icons.travel,
&st().icons.travelActive,
&st().icons.objects,
&st().icons.objectsActive,
&st().icons.symbols,
&st().icons.symbolsActive,
};
const auto index = int(section) * 2 + (active ? 1 : 0);
Assert(index >= 0 && index < icons.size());
return icons[index];
};
const auto paintOne = [&](int left, const style::icon *icon) {
left += (_singleWidth - icon->width()) / 2;
const auto top = (st().footer - icon->height()) / 2;
if (_customTextColor) {
icon->paint(p, left, top, width(), _customTextColor());
} else {
icon->paint(p, left, top, width());
}
};
if (_icons[info.index].setId == AllEmojiSectionSetId()
&& info.width > _singleWidth) {
const auto skip = st::emojiIconSelectSkip;
p.save();
p.setClipRect(
skip,
_iconsTop,
info.width - 2 * skip,
st().footer,
Qt::IntersectClip);
enumerateSubicons([&](const IconInfo &info) {
if (info.visible) {
paintOne(
info.adjustedLeft,
sectionIcon(
Section(int(Section::People) + info.index),
(_subiconState.selected == info.index)));
}
return true;
});
p.restore();
} else {
paintOne(0, [&] {
const auto selected = (info.index == _iconState.selected);
if (icon.setId == AllEmojiSectionSetId()) {
return &st().icons.people;
} else if (const auto section = SetIdEmojiSection(icon.setId)) {
return sectionIcon(*section, selected);
}
return sectionIcon(Section::Recent, selected);
}());
}
}
}
LocalStickersManager::LocalStickersManager(not_null<Main::Session*> session)
: _session(session)
, _api(&session->mtp()) {
}
void LocalStickersManager::install(uint64 setId) {
const auto &sets = _session->data().stickers().sets();
const auto it = sets.find(setId);
if (it == sets.cend()) {
return;
}
const auto set = it->second.get();
const auto input = set->mtpInput();
if (!(set->flags & Data::StickersSetFlag::NotLoaded)
&& !set->stickers.empty()) {
sendInstallRequest(setId, input);
return;
}
_api.request(MTPmessages_GetStickerSet(
input,
MTP_int(0) // hash
)).done([=](const MTPmessages_StickerSet &result) {
result.match([&](const MTPDmessages_stickerSet &data) {
_session->data().stickers().feedSetFull(data);
}, [](const MTPDmessages_stickerSetNotModified &) {
LOG(("API Error: Unexpected messages.stickerSetNotModified."));
});
sendInstallRequest(setId, input);
}).send();
}
bool LocalStickersManager::isInstalledLocally(uint64 setId) const {
return _installedLocallySets.contains(setId);
}
void LocalStickersManager::sendInstallRequest(
uint64 setId,
const MTPInputStickerSet &input) {
_api.request(MTPmessages_InstallStickerSet(
input,
MTP_bool(false)
)).done([=](const MTPmessages_StickerSetInstallResult &result) {
if (result.type() == mtpc_messages_stickerSetInstallResultArchive) {
_session->data().stickers().applyArchivedResult(
result.c_messages_stickerSetInstallResultArchive());
}
}).fail([=] {
notInstalledLocally(setId);
_session->data().stickers().undoInstallLocally(setId);
}).send();
installedLocally(setId);
_session->data().stickers().installLocally(setId);
}
void LocalStickersManager::installedLocally(uint64 setId) {
_installedLocallySets.insert(setId);
}
void LocalStickersManager::notInstalledLocally(uint64 setId) {
_installedLocallySets.remove(setId);
}
void LocalStickersManager::removeInstalledLocally(uint64 setId) {
_installedLocallySets.remove(setId);
}
bool LocalStickersManager::clearInstalledLocally() {
if (_installedLocallySets.empty()) {
return false;
}
_installedLocallySets.clear();
return true;
}
} // namespace ChatHelpers