/* 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/emoji_list_widget.h" #include "api/api_peer_photo.h" #include "apiwrap.h" #include "base/unixtime.h" #include "ui/boxes/confirm_box.h" #include "ui/controls/tabbed_search.h" #include "ui/text/format_values.h" #include "ui/effects/animations.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/menu/menu_add_action_callback_factory.h" #include "ui/widgets/buttons.h" #include "ui/widgets/popup_menu.h" #include "ui/widgets/shadow.h" #include "ui/text/custom_emoji_instance.h" #include "ui/effects/ripple_animation.h" #include "ui/effects/premium_graphics.h" #include "ui/emoji_config.h" #include "ui/painter.h" #include "ui/power_saving.h" #include "ui/ui_utility.h" #include "ui/cached_round_corners.h" #include "boxes/sticker_set_box.h" #include "lang/lang_keys.h" #include "layout/layout_position.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_channel.h" #include "data/data_document.h" #include "data/data_file_origin.h" #include "data/data_peer_values.h" #include "data/stickers/data_stickers.h" #include "data/stickers/data_custom_emoji.h" #include "chat_helpers/emoji_keywords.h" #include "chat_helpers/stickers_list_widget.h" #include "chat_helpers/stickers_list_footer.h" #include "emoji_suggestions_data.h" #include "emoji_suggestions_helper.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "core/core_settings.h" #include "core/application.h" #include "settings/settings_premium.h" #include "window/window_session_controller.h" #include "styles/style_chat_helpers.h" #include "styles/style_menu_icons.h" #include namespace ChatHelpers { namespace { constexpr auto kCollapsedRows = 3; constexpr auto kAppearDuration = 0.3; constexpr auto kCustomSearchLimit = 256; constexpr auto kColorPickerDelay = crl::time(500); using Core::RecentEmojiId; using Core::RecentEmojiDocument; } // namespace class EmojiColorPicker final : public Ui::RpWidget { public: EmojiColorPicker(QWidget *parent, const style::EmojiPan &st); void showEmoji(EmojiPtr emoji, bool allLabel = false); void clearSelection(); void handleMouseMove(QPoint globalPos); void handleMouseRelease(QPoint globalPos); void setSingleSize(QSize size); void showAnimated(); void hideAnimated(); void hideFast(); [[nodiscard]] rpl::producer chosen() const; [[nodiscard]] rpl::producer<> hidden() const; protected: void paintEvent(QPaintEvent *e) override; void mousePressEvent(QMouseEvent *e) override; void mouseReleaseEvent(QMouseEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override; private: void createAllLabel(); void animationCallback(); void updateSize(); [[nodiscard]] int topColorAllSkip() const; void drawVariant(QPainter &p, int variant); void updateSelected(); void setSelected(int newSelected); const style::EmojiPan &_st; bool _ignoreShow = false; QVector _variants; int _selected = -1; int _pressedSel = -1; QPoint _lastMousePos; QSize _singleSize; QPoint _areaPosition; QPoint _innerPosition; Ui::RoundRect _backgroundRect; Ui::RoundRect _overBg; bool _hiding = false; QPixmap _cache; Ui::Animations::Simple _a_opacity; std::unique_ptr _allLabel; rpl::event_stream _chosen; rpl::event_stream<> _hidden; }; struct EmojiListWidget::CustomEmojiInstance { std::unique_ptr emoji; bool recentOnly = false; }; struct EmojiListWidget::RecentOne { Ui::Text::CustomEmoji *custom = nullptr; RecentEmojiId id; }; EmojiColorPicker::EmojiColorPicker( QWidget *parent, const style::EmojiPan &st) : RpWidget(parent) , _st(st) , _backgroundRect(st::emojiPanRadius, _st.bg) , _overBg(st::emojiPanRadius, _st.overBg) { setMouseTracking(true); } void EmojiColorPicker::showEmoji(EmojiPtr emoji, bool allLabel) { if (!emoji || !emoji->hasVariants()) { return; } if (!allLabel) { _allLabel = nullptr; } else if (!_allLabel) { createAllLabel(); } _ignoreShow = false; _variants.resize(emoji->variantsCount() + 1); for (auto i = 0, size = int(_variants.size()); i != size; ++i) { _variants[i] = emoji->variant(i); } updateSize(); if (!_cache.isNull()) { _cache = QPixmap(); } showAnimated(); } void EmojiColorPicker::createAllLabel() { _allLabel = std::make_unique( this, tr::lng_emoji_color_all(), _st.colorAllLabel); _allLabel->show(); _allLabel->setAttribute(Qt::WA_TransparentForMouseEvents); } void EmojiColorPicker::updateSize() { auto width = st::emojiPanMargins.left() + _singleSize.width() * _variants.size() + (_variants.size() - 2) * st::emojiColorsPadding + st::emojiColorsSep + st::emojiPanMargins.right(); auto height = st::emojiPanMargins.top() + 2 * st::emojiColorsPadding + _singleSize.height() + st::emojiPanMargins.bottom(); if (_allLabel) { _allLabel->resizeToWidth(width - st::emojiPanMargins.left() - st::emojiPanMargins.right() - st::emojiPanColorAllPadding.left() - st::emojiPanColorAllPadding.right()); _allLabel->move( st::emojiPanMargins.left() + st::emojiPanColorAllPadding.left(), st::emojiPanMargins.top() + st::emojiPanColorAllPadding.top()); height += topColorAllSkip(); } resize(width, height); update(); updateSelected(); } void EmojiColorPicker::paintEvent(QPaintEvent *e) { auto p = QPainter(this); auto opacity = _a_opacity.value(_hiding ? 0. : 1.); if (opacity < 1.) { if (opacity > 0.) { p.setOpacity(opacity); } else { return; } } if (e->rect() != rect()) { p.setClipRect(e->rect()); } auto inner = rect().marginsRemoved(st::emojiPanMargins); if (!_cache.isNull()) { p.drawPixmap(0, 0, _cache); return; } Ui::Shadow::paint(p, inner, width(), _st.showAnimation.shadow); _backgroundRect.paint(p, inner); const auto skip = topColorAllSkip(); auto x = st::emojiPanMargins.left() + 2 * st::emojiColorsPadding + _singleSize.width(); if (rtl()) x = width() - x - st::emojiColorsSep; p.fillRect(x, st::emojiPanMargins.top() + skip + st::emojiColorsPadding, st::emojiColorsSep, inner.height() - st::emojiColorsPadding * 2 - skip, st::emojiColorsSepColor); if (_variants.isEmpty()) { return; } p.translate(0, skip); for (auto i = 0, count = int(_variants.size()); i != count; ++i) { drawVariant(p, i); } } void EmojiColorPicker::mousePressEvent(QMouseEvent *e) { if (e->button() != Qt::LeftButton) { return; } _lastMousePos = e->globalPos(); updateSelected(); _pressedSel = _selected; } void EmojiColorPicker::mouseReleaseEvent(QMouseEvent *e) { handleMouseRelease(e->globalPos()); } void EmojiColorPicker::handleMouseRelease(QPoint globalPos) { _lastMousePos = globalPos; int32 pressed = _pressedSel; _pressedSel = -1; updateSelected(); if (_selected >= 0 && (pressed < 0 || _selected == pressed)) { _chosen.fire_copy({ .emoji = _variants[_selected] }); } _ignoreShow = true; hideAnimated(); } void EmojiColorPicker::setSingleSize(QSize size) { const auto area = st::emojiPanArea; _singleSize = size; _areaPosition = QPoint( (_singleSize.width() - area.width()) / 2, (_singleSize.height() - area.height()) / 2); const auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio(); _innerPosition = QPoint( (area.width() - esize) / 2, (area.height() - esize) / 2); updateSize(); } void EmojiColorPicker::handleMouseMove(QPoint globalPos) { _lastMousePos = globalPos; updateSelected(); } void EmojiColorPicker::mouseMoveEvent(QMouseEvent *e) { handleMouseMove(e->globalPos()); } void EmojiColorPicker::animationCallback() { update(); if (!_a_opacity.animating()) { _cache = QPixmap(); if (_allLabel) { _allLabel->show(); } if (_hiding) { hide(); _hidden.fire({}); } else { _lastMousePos = QCursor::pos(); updateSelected(); } } } void EmojiColorPicker::hideFast() { clearSelection(); _a_opacity.stop(); _cache = QPixmap(); hide(); _hidden.fire({}); } rpl::producer EmojiColorPicker::chosen() const { return _chosen.events(); } rpl::producer<> EmojiColorPicker::hidden() const { return _hidden.events(); } void EmojiColorPicker::hideAnimated() { if (_cache.isNull()) { if (_allLabel) { _allLabel->show(); } _cache = Ui::GrabWidget(this); clearSelection(); } _hiding = true; if (_allLabel) { _allLabel->hide(); } _a_opacity.start([this] { animationCallback(); }, 1., 0., st::emojiPanDuration); } void EmojiColorPicker::showAnimated() { if (_ignoreShow) return; if (!isHidden() && !_hiding) { return; } _hiding = false; if (_cache.isNull()) { if (_allLabel) { _allLabel->show(); } _cache = Ui::GrabWidget(this); clearSelection(); } show(); if (_allLabel) { _allLabel->hide(); } _a_opacity.start([this] { animationCallback(); }, 0., 1., st::emojiPanDuration); } void EmojiColorPicker::clearSelection() { _pressedSel = -1; setSelected(-1); _lastMousePos = mapToGlobal(QPoint(-10, -10)); } int EmojiColorPicker::topColorAllSkip() const { return _allLabel ? (st::emojiPanColorAllPadding.top() + _allLabel->height() + st::emojiPanColorAllPadding.bottom()) : 0; } void EmojiColorPicker::updateSelected() { auto newSelected = -1; auto p = mapFromGlobal(_lastMousePos); auto sx = rtl() ? (width() - p.x()) : p.x(), y = p.y() - st::emojiPanMargins.top() - topColorAllSkip() - st::emojiColorsPadding; if (y >= 0 && y < _singleSize.height()) { auto x = sx - st::emojiPanMargins.left() - st::emojiColorsPadding; if (x >= 0 && x < _singleSize.width()) { newSelected = 0; } else { x -= _singleSize.width() + 2 * st::emojiColorsPadding + st::emojiColorsSep; if (x >= 0 && x < _singleSize.width() * (_variants.size() - 1)) { newSelected = (x / _singleSize.width()) + 1; } } } setSelected(newSelected); } void EmojiColorPicker::setSelected(int newSelected) { if (_selected == newSelected) { return; } const auto skip = topColorAllSkip(); const auto updateSelectedRect = [&] { if (_selected < 0) return; auto addedSkip = (_selected > 0) ? (2 * st::emojiColorsPadding + st::emojiColorsSep) : 0; auto left = st::emojiPanMargins.left() + st::emojiColorsPadding + _selected * _singleSize.width() + addedSkip; rtlupdate( left, st::emojiPanMargins.top() + st::emojiColorsPadding + skip, _singleSize.width(), _singleSize.height()); }; updateSelectedRect(); _selected = newSelected; updateSelectedRect(); setCursor((_selected >= 0) ? style::cur_pointer : style::cur_default); } void EmojiColorPicker::drawVariant(QPainter &p, int variant) { const auto w = QPoint( st::emojiPanMargins.left(), st::emojiPanMargins.top() ) + QPoint( (st::emojiColorsPadding + variant * _singleSize.width() + (variant ? (2 * st::emojiColorsPadding + st::emojiColorsSep) : 0)), st::emojiColorsPadding ) + _areaPosition; if (variant == _selected) { QPoint tl(w); if (rtl()) tl.setX(width() - tl.x() - st::emojiPanArea.width()); _overBg.paint(p, QRect(tl, st::emojiPanArea)); } Ui::Emoji::Draw( p, _variants[variant], Ui::Emoji::GetSizeLarge(), w.x() + _innerPosition.x(), w.y() + _innerPosition.y()); } EmojiListWidget::EmojiListWidget( QWidget *parent, not_null controller, PauseReason level, Mode mode) : EmojiListWidget(parent, { .show = controller->uiShow(), .mode = mode, .paused = Window::PausedIn(controller, level), }) { } EmojiListWidget::EmojiListWidget( QWidget *parent, EmojiListDescriptor &&descriptor) : Inner( parent, descriptor.st ? *descriptor.st : st::defaultEmojiPan, descriptor.show, std::move(descriptor.paused)) , _show(std::move(descriptor.show)) , _features(descriptor.features) , _onlyUnicodeEmoji(descriptor.mode == Mode::PeerTitle) , _mode(_onlyUnicodeEmoji ? Mode::Full : descriptor.mode) , _api(&session().mtp()) , _staticCount(_mode == Mode::Full ? kEmojiSectionCount : 1) , _premiumIcon(_mode == Mode::EmojiStatus ? std::make_unique() : nullptr) , _localSetsManager( std::make_unique(&session())) , _customRecentFactory(std::move(descriptor.customRecentFactory)) , _customTextColor(std::move(descriptor.customTextColor)) , _overBg(st::emojiPanRadius, st().overBg) , _collapsedBg(st::emojiPanExpand.height / 2, st().headerFg) , _picker(this, st()) , _showPickerTimer([=] { showPicker(); }) , _previewTimer([=] { showPreview(); }) { setMouseTracking(true); if (st().bg->c.alpha() > 0) { setAttribute(Qt::WA_OpaquePaintEvent); } if (_mode != Mode::RecentReactions && _mode != Mode::BackgroundEmoji && _mode != Mode::ChannelStatus && !_onlyUnicodeEmoji) { setupSearch(); } if (_mode == Mode::ChannelStatus) { session().api().peerPhoto().emojiListValue( Api::PeerPhoto::EmojiListType::NoChannelStatus ) | rpl::start_with_next([=](const std::vector &list) { _restrictedCustomList = { begin(list), end(list) }; if (!_custom.empty()) { refreshCustom(); } }, lifetime()); } _customSingleSize = Data::FrameSizeFromTag( Data::CustomEmojiManager::SizeTag::Large ) / style::DevicePixelRatio(); _picker->hide(); for (auto i = 1; i != _staticCount; ++i) { const auto section = static_cast
(i); _counts[i] = Ui::Emoji::GetSectionCount(section); } _picker->chosen( ) | rpl::start_with_next([=](EmojiChosen data) { colorChosen(data); }, lifetime()); _picker->hidden( ) | rpl::start_with_next([=] { pickerHidden(); }, lifetime()); session().changes().peerUpdates( Data::PeerUpdate::Flag::EmojiSet ) | rpl::filter([=](const Data::PeerUpdate &update) { return (update.peer.get() == _megagroupSet); }) | rpl::start_with_next([=] { refreshCustom(); resizeToWidth(width()); }, lifetime()); session().data().stickers().updated( Data::StickersType::Emoji ) | rpl::start_with_next([=] { refreshCustom(); resizeToWidth(width()); }, lifetime()); rpl::combine( Data::AmPremiumValue(&session()), session().premiumPossibleValue() ) | rpl::skip(1) | rpl::start_with_next([=] { refreshCustom(); resizeToWidth(width()); }, lifetime()); rpl::single( rpl::empty ) | rpl::then( style::PaletteChanged() ) | rpl::start_with_next([=] { initButton(_add, tr::lng_stickers_featured_add(tr::now), false); initButton(_unlock, tr::lng_emoji_featured_unlock(tr::now), true); initButton(_restore, tr::lng_emoji_premium_restore(tr::now), true); }, lifetime()); if (!descriptor.customRecentList.empty()) { fillRecentFrom(descriptor.customRecentList); } } EmojiListWidget::~EmojiListWidget() { base::take(_customEmoji); } void EmojiListWidget::setupSearch() { const auto session = &_show->session(); const auto type = (_mode == Mode::EmojiStatus) ? TabbedSearchType::Status : (_mode == Mode::UserpicBuilder) ? TabbedSearchType::ProfilePhoto : TabbedSearchType::Emoji; _search = MakeSearch(this, st(), [=](std::vector &&query) { _nextSearchQuery = std::move(query); InvokeQueued(this, [=] { applyNextSearchQuery(); }); }, session, type); } void EmojiListWidget::applyNextSearchQuery() { if (_searchQuery == _nextSearchQuery) { return; } _searchQuery = _nextSearchQuery; std::swap(_searchEmoji, _searchEmojiPrevious); _searchEmoji.clear(); const auto finish = [&](bool searching = true) { if (!_searchMode && !searching) { return; } const auto modeChanged = (_searchMode != searching); clearSelection(); if (modeChanged) { _searchMode = searching; } if (!searching) { _searchResults.clear(); _searchCustomIds.clear(); } resizeToWidth(width()); update(); if (modeChanged) { visibleTopBottomUpdated(getVisibleTop(), getVisibleBottom()); } updateSelected(); }; if (_searchQuery.empty()) { finish(false); return; } const auto guard = gsl::finally([&] { finish(); }); auto plain = collectPlainSearchResults(); if (_searchEmoji == _searchEmojiPrevious) { return; } _searchResults.clear(); _searchCustomIds.clear(); if (_mode != Mode::Full || session().premium()) { appendPremiumSearchResults(); } if (_mode == Mode::Full) { for (const auto emoji : plain) { _searchResults.push_back({ .id = { emoji }, }); } } } void EmojiListWidget::showPreview() { if (const auto over = std::get_if(&_pressed)) { if (const auto custom = lookupCustomEmoji(over)) { _show->showMediaPreview(custom->stickerSetOrigin(), custom); _previewShown = true; } } } std::vector EmojiListWidget::collectPlainSearchResults() { return SearchEmoji(_searchQuery, _searchEmoji); } void EmojiListWidget::appendPremiumSearchResults() { const auto test = session().isTestMode(); auto &owner = session().data(); const auto checkCustom = [&](EmojiPtr emoji, DocumentId id) { return emoji && _searchEmoji.contains(emoji) && (_searchResults.size() < kCustomSearchLimit) && _searchCustomIds.emplace(id).second; }; for (const auto &recent : _recent) { if (!recent.custom) { continue; } const auto &idData = recent.id.data; const auto id = std::get_if(&idData); if (!id || id->test != test) { continue; } const auto sticker = owner.document(id->id)->sticker(); const auto emoji = sticker ? Ui::Emoji::Find(sticker->alt) : nullptr; if (checkCustom(emoji, id->id)) { _searchResults.push_back(recent); } } for (const auto &set : _custom) { for (const auto &one : set.list) { const auto id = one.document->id; if (checkCustom(one.emoji, id)) { _searchResults.push_back({ .custom = one.custom, .id = { RecentEmojiDocument{ .id = id, .test = test } }, }); } } } } void EmojiListWidget::provideRecent( const std::vector &customRecentList) { clearSelection(); fillRecentFrom(customRecentList); resizeToWidth(width()); } void EmojiListWidget::repaintCustom(uint64 setId) { if (!_repaintsScheduled.emplace(setId).second) { return; } const auto repaintSearch = (setId == SearchEmojiSectionSetId()); if (_searchMode) { if (repaintSearch) { update(); } return; } const auto repaintRecent = (setId == RecentEmojiSectionSetId()); enumerateSections([&](const SectionInfo &info) { const auto repaint1 = repaintRecent && (info.section == int(Section::Recent)); const auto repaint2 = !repaint1 && (info.section >= _staticCount) && (setId == _custom[info.section - _staticCount].id); if (repaint1 || repaint2) { update( 0, info.rowsTop, width(), info.rowsBottom - info.rowsTop); } return true; }); } rpl::producer EmojiListWidget::chosen() const { return _chosen.events(); } rpl::producer EmojiListWidget::customChosen() const { return _customChosen.events(); } rpl::producer<> EmojiListWidget::jumpedToPremium() const { return _jumpedToPremium.events(); } rpl::producer<> EmojiListWidget::escapes() const { return _search ? _search->escapes() : rpl::never<>(); } void EmojiListWidget::prepareExpanding() { if (_search) { _searchExpandCache = _search->grab(); } } void EmojiListWidget::paintExpanding( Painter &p, QRect clip, int finalBottom, float64 geometryProgress, float64 fullProgress, RectPart origin) { const auto searchShift = _search ? anim::interpolate( st().padding.top() - _search->height(), 0, geometryProgress) : 0; const auto shift = clip.topLeft() + QPoint(0, searchShift); const auto adjusted = clip.translated(-shift); const auto finalHeight = (finalBottom - clip.y()); if (!_searchExpandCache.isNull()) { p.setClipRect(clip); p.drawImage( clip.x() + st().searchMargin.left(), clip.y() + st().searchMargin.top() + searchShift, _searchExpandCache); } p.translate(shift); p.setClipRect(adjusted); paint(p, ExpandingContext{ .progress = fullProgress, .finalHeight = finalHeight, .expanding = true, }, adjusted); p.translate(-shift); } void EmojiListWidget::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { Inner::visibleTopBottomUpdated(visibleTop, visibleBottom); if (_footer) { _footer->validateSelectedIcon( currentSet(visibleTop), ValidateIconAnimations::Full); } unloadNotSeenCustom(visibleTop, visibleBottom); } void EmojiListWidget::unloadNotSeenCustom( int visibleTop, int visibleBottom) { enumerateSections([&](const SectionInfo &info) { if (info.rowsBottom <= visibleTop || info.rowsTop >= visibleBottom) { unloadCustomIn(info); } return true; }); } void EmojiListWidget::unloadAllCustom() { enumerateSections([&](const SectionInfo &info) { unloadCustomIn(info); return true; }); } void EmojiListWidget::unloadCustomIn(const SectionInfo &info) { if (!info.section && _recentPainted) { _recentPainted = false; for (const auto &single : _recent) { if (const auto custom = single.custom) { custom->unload(); } } return; } else if (info.section < _staticCount) { return; } auto &custom = _custom[info.section - _staticCount]; if (!custom.painted) { return; } custom.painted = false; for (const auto &single : custom.list) { single.custom->unload(); } } object_ptr EmojiListWidget::createFooter() { Expects(_footer == nullptr); if (_mode == EmojiListMode::RecentReactions) { return { nullptr }; } using FooterDescriptor = StickersListFooter::Descriptor; const auto flag = powerSavingFlag(); const auto footerPaused = [method = pausedMethod(), flag]() { return On(flag) || method(); }; auto result = object_ptr(FooterDescriptor{ .session = &session(), .customTextColor = _customTextColor, .paused = footerPaused, .parent = this, .st = &st(), .features = { .stickersSettings = false }, .forceFirstFrame = (_mode == Mode::BackgroundEmoji), }); _footer = result; _footer->setChosen( ) | rpl::start_with_next([=](uint64 setId) { showSet(setId); }, _footer->lifetime()); return result; } void EmojiListWidget::afterShown() { const auto steal = (_mode == Mode::EmojiStatus) || (_mode == Mode::FullReactions) || (_mode == Mode::UserpicBuilder); if (_search && steal) { _search->stealFocus(); } } void EmojiListWidget::beforeHiding() { if (_search) { _search->returnFocus(); } } template bool EmojiListWidget::enumerateSections(Callback callback) const { Expects(_columnCount > 0); auto i = 0; auto info = SectionInfo(); const auto next = [&] { info.rowsCount = info.collapsed ? kCollapsedRows : (info.count + _columnCount - 1) / _columnCount; info.rowsTop = info.top + (i == 0 ? _rowsTop : st().header); info.rowsBottom = info.rowsTop + (info.rowsCount * _singleSize.height()); if (!callback(info)) { return false; } info.top = info.rowsBottom; return true; }; if (_searchMode) { info.section = i; info.count = _searchResults.size(); return next(); } for (; i != _staticCount; ++i) { info.section = i; info.count = i ? _counts[i] : _recent.size(); if (!next()) { return false; } } for (auto §ion : _custom) { info.section = i++; info.premiumRequired = section.premiumRequired; info.count = int(section.list.size()); info.collapsed = !section.expanded && (!section.canRemove || section.premiumRequired) && (info.count > _columnCount * kCollapsedRows); if (!next()) { return false; } } return true; } EmojiListWidget::SectionInfo EmojiListWidget::sectionInfo(int section) const { Expects(section >= 0 && section < sectionsCount()); auto result = SectionInfo(); enumerateSections([&](const SectionInfo &info) { if (info.section == section) { result = info; return false; } return true; }); return result; } EmojiListWidget::SectionInfo EmojiListWidget::sectionInfoByOffset( int yOffset) const { auto result = SectionInfo(); const auto count = sectionsCount(); enumerateSections([&result, count, yOffset](const SectionInfo &info) { if (yOffset < info.rowsBottom || info.section == count - 1) { result = info; return false; } return true; }); return result; } int EmojiListWidget::sectionsCount() const { return _searchMode ? 1 : (_staticCount + int(_custom.size())); } void EmojiListWidget::setSingleSize(QSize size) { const auto area = st::emojiPanArea; _singleSize = size; _areaPosition = QPoint( (_singleSize.width() - area.width()) / 2, (_singleSize.height() - area.height()) / 2); const auto esize = Ui::Emoji::GetSizeLarge() / style::DevicePixelRatio(); _innerPosition = QPoint( (area.width() - esize) / 2, (area.height() - esize) / 2); const auto customSkip = (esize - _customSingleSize) / 2; _customPosition = QPoint(customSkip, customSkip); _picker->setSingleSize(_singleSize); } void EmojiListWidget::setColorAllForceRippled(bool force) { _colorAllRippleForced = force; if (_colorAllRippleForced) { _colorAllRippleForcedLifetime = style::PaletteChanged( ) | rpl::filter([=] { return _colorAllRipple != nullptr; }) | rpl::start_with_next([=] { _colorAllRipple->forceRepaint(); }); if (!_colorAllRipple) { _colorAllRipple = createButtonRipple(int(Section::People)); } if (_colorAllRipple->empty()) { _colorAllRipple->addFading(); } else { _colorAllRipple->lastUnstop(); } } else { if (_colorAllRipple) { _colorAllRipple->lastStop(); } _colorAllRippleForcedLifetime.destroy(); } } int EmojiListWidget::countDesiredHeight(int newWidth) { const auto fullWidth = st().margin.left() + newWidth + st().margin.right(); const auto padding = st().padding; const auto innerWidth = fullWidth - padding.left() - padding.right(); _columnCount = std::max(innerWidth / st().desiredSize, 1); const auto singleWidth = innerWidth / _columnCount; _rowsTop = _search ? _search->height() : padding.top(); _rowsLeft = padding.left() + (innerWidth - _columnCount * singleWidth) / 2 - st().margin.left(); setSingleSize({ singleWidth, singleWidth - 2 * st().verticalSizeSub }); const auto countResult = [this](int minimalLastHeight) { const auto info = sectionInfo(sectionsCount() - 1); return info.top + qMax(info.rowsBottom - info.top, minimalLastHeight); }; const auto minimalHeight = this->minimalHeight(); const auto minimalLastHeight = std::max( minimalHeight - padding.bottom(), 0); return qMax( minimalHeight, countResult(minimalLastHeight) + padding.bottom()); } int EmojiListWidget::defaultMinimalHeight() const { return Inner::defaultMinimalHeight(); } void EmojiListWidget::ensureLoaded(int section) { Expects(section >= 0 && section < sectionsCount()); if (section == int(Section::Recent)) { if (_recent.empty()) { fillRecent(); } return; } else if (section >= _staticCount || !_emoji[section].empty()) { return; } _emoji[section] = Ui::Emoji::GetSection(static_cast
(section)); _counts[section] = _emoji[section].size(); const auto &settings = Core::App().settings(); for (auto &emoji : _emoji[section]) { emoji = settings.lookupEmojiVariant(emoji); } } void EmojiListWidget::fillRecent() { if (_mode != Mode::Full) { return; } _recent.clear(); _recentCustomIds.clear(); const auto &list = Core::App().settings().recentEmoji(); _recent.reserve(std::min(int(list.size()), Core::kRecentEmojiLimit) + 1); const auto test = session().isTestMode(); for (const auto &one : list) { const auto document = std::get_if(&one.id.data); if (document && ((document->test != test) || _onlyUnicodeEmoji)) { continue; } _recent.push_back({ .custom = resolveCustomRecent(one.id), .id = one.id, }); if (document) { _recentCustomIds.emplace(document->id); } if (_recent.size() >= Core::kRecentEmojiLimit) { break; } } } void EmojiListWidget::fillRecentFrom(const std::vector &list) { const auto test = session().isTestMode(); _recent.clear(); _recent.reserve(list.size()); for (const auto &id : list) { if (!id && _mode == Mode::EmojiStatus) { const auto star = QString::fromUtf8("\xe2\xad\x90\xef\xb8\x8f"); _recent.push_back({ .id = { Ui::Emoji::Find(star) } }); } else if (!id && (_mode == Mode::BackgroundEmoji || _mode == Mode::ChannelStatus)) { const auto fakeId = DocumentId(5246772116543512028ULL); const auto no = QString::fromUtf8("\xe2\x9b\x94\xef\xb8\x8f"); _recent.push_back({ .custom = resolveCustomRecent(fakeId), .id = { Ui::Emoji::Find(no) }, }); _recentCustomIds.emplace(fakeId); } else { _recent.push_back({ .custom = resolveCustomRecent(id), .id = { RecentEmojiDocument{ .id = id, .test = test } }, }); _recentCustomIds.emplace(id); } } } base::unique_qptr EmojiListWidget::fillContextMenu( SendMenu::Type type) { if (v::is_null(_selected)) { return nullptr; } const auto over = std::get_if(&_selected); if (!over) { return nullptr; } const auto section = over->section; const auto index = over->index; auto menu = base::make_unique_q( this, (_mode == Mode::Full ? st::popupMenuWithIcons : st::defaultPopupMenu)); if (_mode == Mode::Full) { fillRecentMenu(menu, section, index); } else if (_mode == Mode::EmojiStatus || _mode == Mode::ChannelStatus) { fillEmojiStatusMenu(menu, section, index); } if (menu->empty()) { return nullptr; } return menu; } void EmojiListWidget::fillRecentMenu( not_null menu, int section, int index) { if (section != int(Section::Recent)) { return; } const auto addAction = Ui::Menu::CreateAddActionCallback(menu); const auto over = OverEmoji{ section, index }; const auto emoji = lookupOverEmoji(&over); const auto custom = lookupCustomEmoji(&over); if (custom && custom->sticker()) { const auto sticker = custom->sticker(); const auto emoji = sticker->alt; const auto setId = sticker->set.id; if (!emoji.isEmpty()) { auto data = TextForMimeData{ emoji, { emoji } }; data.rich.entities.push_back({ EntityType::CustomEmoji, 0, int(emoji.size()), Data::SerializeCustomEmojiId(custom) }); addAction(tr::lng_emoji_copy(tr::now), [=] { TextUtilities::SetClipboardText(data); }, &st::menuIconCopy); } if (setId && _features.openStickerSets) { addAction( tr::lng_emoji_view_pack(tr::now), crl::guard(this, [=] { displaySet(setId); }), &st::menuIconShowAll); } } else if (emoji) { addAction(tr::lng_emoji_copy(tr::now), [=] { const auto text = emoji->text(); TextUtilities::SetClipboardText({ text, { text } }); }, &st::menuIconCopy); } auto id = RecentEmojiId{ emoji }; if (custom) { id.data = RecentEmojiDocument{ .id = custom->id, .test = custom->session().isTestMode(), }; } addAction(tr::lng_emoji_remove_recent(tr::now), crl::guard(this, [=] { Core::App().settings().hideRecentEmoji(id); refreshRecent(); }), &st::menuIconCancel); menu->addSeparator(&st().expandedSeparator); const auto resetRecent = [=] { const auto sure = [=](Fn &&close) { Core::App().settings().resetRecentEmoji(); refreshRecent(); close(); }; checkHideWithBox(Ui::MakeConfirmBox({ .text = tr::lng_emoji_reset_recent_sure(), .confirmed = crl::guard(this, sure), .confirmText = tr::lng_emoji_reset_recent_button(tr::now), .labelStyle = &st().boxLabel, })); }; addAction({ .text = tr::lng_emoji_reset_recent(tr::now), .handler = crl::guard(this, resetRecent), .icon = &st::menuIconRestoreAttention, .isAttention = true, }); } void EmojiListWidget::fillEmojiStatusMenu( not_null menu, int section, int index) { const auto chosen = lookupCustomEmoji(index, section); if (!chosen) { return; } const auto selectWith = [=](TimeId scheduled) { selectCustom( lookupChosen(chosen, nullptr, { .scheduled = scheduled })); }; for (const auto &value : { 3600, 3600 * 8, 3600 * 24, 3600 * 24 * 7 }) { const auto text = tr::lng_emoji_status_menu_duration_any( tr::now, lt_duration, Ui::FormatMuteFor(value)); menu->addAction(text, crl::guard(this, [=] { selectWith(base::unixtime::now() + value); })); } menu->addAction( tr::lng_manage_messages_ttl_after_custom(tr::now), crl::guard(this, [=] { selectWith( TabbedSelector::kPickCustomTimeId); })); } void EmojiListWidget::paintEvent(QPaintEvent *e) { auto p = Painter(this); const auto clip = e ? e->rect() : rect(); _repaintsScheduled.clear(); if (_grabbingChosen) { p.setCompositionMode(QPainter::CompositionMode_Source); p.fillRect(clip, Qt::transparent); p.setCompositionMode(QPainter::CompositionMode_SourceOver); } else if (st().bg->c.alpha() > 0) { p.fillRect(clip, st().bg); } if (!_searchExpandCache.isNull()) { _searchExpandCache = QImage(); } paint(p, {}, clip); } void EmojiListWidget::validateEmojiPaintContext( const ExpandingContext &context) { auto value = Ui::Text::CustomEmojiPaintContext{ .textColor = (_customTextColor ? _customTextColor() : (_mode == Mode::EmojiStatus || _mode == Mode::ChannelStatus) ? anim::color( st::stickerPanPremium1, st::stickerPanPremium2, 0.5) : st().textFg->c), .size = QSize(_customSingleSize, _customSingleSize), .now = crl::now(), .scale = context.progress, .paused = On(powerSavingFlag()) || paused(), .scaled = context.expanding, .internal = { .forceFirstFrame = (_mode == Mode::BackgroundEmoji) }, }; if (!_emojiPaintContext) { _emojiPaintContext = std::make_unique< Ui::Text::CustomEmojiPaintContext >(std::move(value)); } else { *_emojiPaintContext = std::move(value); } } void EmojiListWidget::paint( Painter &p, ExpandingContext context, QRect clip) { validateEmojiPaintContext(context); auto fromColumn = floorclamp( clip.x() - _rowsLeft, _singleSize.width(), 0, _columnCount); auto toColumn = ceilclamp( clip.x() + clip.width() - _rowsLeft, _singleSize.width(), 0, _columnCount); if (rtl()) { qSwap(fromColumn, toColumn); fromColumn = _columnCount - fromColumn; toColumn = _columnCount - toColumn; } const auto expandProgress = context.progress; auto selectedButton = std::get_if(!v::is_null(_pressed) ? &_pressed : &_selected); if (_searchResults.empty() && _searchMode) { paintEmptySearchResults(p); } enumerateSections([&](const SectionInfo &info) { if (clip.top() >= info.rowsBottom) { return true; } else if (clip.top() + clip.height() <= info.top) { return false; } const auto buttonSelected = selectedButton ? (selectedButton->section == info.section) : false; const auto titleLeft = (info.premiumRequired ? st().headerLockedLeft : st().headerLeft) - st().margin.left(); const auto widthForTitle = emojiRight() - titleLeft - paintButtonGetWidth(p, info, buttonSelected, clip); if (info.section > 0 && clip.top() < info.rowsTop) { p.setFont(st::emojiPanHeaderFont); p.setPen(st().headerFg); auto titleText = (info.section < _staticCount) ? ChatHelpers::EmojiCategoryTitle(info.section)(tr::now) : _custom[info.section - _staticCount].title; auto titleWidth = st::emojiPanHeaderFont->width(titleText); if (titleWidth > widthForTitle) { titleText = st::emojiPanHeaderFont->elided(titleText, widthForTitle); titleWidth = st::emojiPanHeaderFont->width(titleText); } const auto top = info.top + st().headerTop; if (info.premiumRequired) { st::emojiPremiumRequired.paint( p, st().headerLockLeft - st().margin.left(), top, width()); } const auto textBaseline = top + st::emojiPanHeaderFont->ascent; p.setFont(st::emojiPanHeaderFont); p.setPen(st().headerFg); p.drawText(titleLeft, textBaseline, titleText); } if (clip.top() + clip.height() > info.rowsTop) { ensureLoaded(info.section); auto fromRow = floorclamp( clip.y() - info.rowsTop, _singleSize.height(), 0, info.rowsCount); auto toRow = ceilclamp( clip.y() + clip.height() - info.rowsTop, _singleSize.height(), 0, info.rowsCount); for (auto i = fromRow; i < toRow; ++i) { for (auto j = fromColumn; j < toColumn; ++j) { const auto index = i * _columnCount + j; if (index >= info.count) { break; } const auto state = OverEmoji{ .section = info.section, .index = index, }; const auto selected = (state == _selected) || (!_picker->isHidden() && state == _pickerSelected); const auto position = QPoint( _rowsLeft + j * _singleSize.width(), info.rowsTop + i * _singleSize.height() ); const auto w = position + _areaPosition; if (context.expanding) { const auto y = (position.y() - _rowsTop); const auto x = (position.x() - _rowsLeft); const auto sum = y + std::max(std::min(y, width()) - x, 0); const auto maxSum = context.finalHeight + std::min(context.finalHeight, width()); const auto started = (sum / float64(maxSum)) - kAppearDuration; context.progress = (expandProgress <= started) ? 0. : (expandProgress >= started + kAppearDuration) ? 1. : ((expandProgress - started) / kAppearDuration); } if (info.collapsed && index + 1 == _columnCount * kCollapsedRows) { drawCollapsedBadge(p, w - _areaPosition, info.count); continue; } if (!_grabbingChosen && selected && st().overBg->c.alpha() > 0) { auto tl = w; if (rtl()) { tl.setX(width() - tl.x() - st::emojiPanArea.width()); } _overBg.paint(p, QRect(tl, st::emojiPanArea)); } if (_searchMode) { drawRecent(p, context, w, _searchResults[index]); } else if (info.section == int(Section::Recent)) { drawRecent(p, context, w, _recent[index]); } else if (info.section < _staticCount) { drawEmoji(p, context, w, _emoji[info.section][index]); } else { const auto set = info.section - _staticCount; drawCustom(p, context, w, set, index); } } } } return true; }); } void EmojiListWidget::drawCollapsedBadge( QPainter &p, QPoint position, int count) { const auto &st = st::emojiPanExpand; const auto text = u"+%1"_q.arg(count - _columnCount * kCollapsedRows + 1); const auto textWidth = st.font->width(text); const auto buttonw = std::max(textWidth - st.width, st.height); const auto buttonh = st.height; const auto buttonx = position.x() + (_singleSize.width() - buttonw) / 2; const auto buttony = position.y() + (_singleSize.height() - buttonh) / 2; _collapsedBg.paint(p, QRect(buttonx, buttony, buttonw, buttonh)); p.setPen(this->st().bg); p.setFont(st.font); p.drawText( buttonx + (buttonw - textWidth) / 2, (buttony + st.textTop + st.font->ascent), text); } void EmojiListWidget::drawRecent( QPainter &p, const ExpandingContext &context, QPoint position, const RecentOne &recent) { _recentPainted = true; if (const auto custom = recent.custom) { _emojiPaintContext->scale = context.progress; _emojiPaintContext->position = position + _innerPosition + _customPosition; if (_mode == Mode::ChannelStatus) { _emojiPaintContext->internal.forceFirstFrame = (recent.id == _recent.front().id); } custom->paint(p, *_emojiPaintContext); } else if (const auto emoji = std::get_if(&recent.id.data)) { if (_mode == Mode::EmojiStatus) { position += QPoint( (_singleSize.width() - st::emojiStatusDefault.width()) / 2, (_singleSize.height() - st::emojiStatusDefault.height()) / 2 ) - _areaPosition; p.drawImage(position, _premiumIcon->image()); } else { drawEmoji(p, context, position, *emoji); } } else { Unexpected("Empty custom emoji in EmojiListWidget::drawRecent."); } } void EmojiListWidget::drawEmoji( QPainter &p, const ExpandingContext &context, QPoint position, EmojiPtr emoji) { position += _innerPosition; Ui::Emoji::Draw( p, emoji, Ui::Emoji::GetSizeLarge(), position.x(), position.y()); } void EmojiListWidget::drawCustom( QPainter &p, const ExpandingContext &context, QPoint position, int set, int index) { auto &custom = _custom[set]; custom.painted = true; auto &entry = custom.list[index]; _emojiPaintContext->scale = context.progress; _emojiPaintContext->position = position + _innerPosition + _customPosition; entry.custom->paint(p, *_emojiPaintContext); } bool EmojiListWidget::checkPickerHide() { if (!_picker->isHidden() && !v::is_null(_pickerSelected)) { _picker->hideAnimated(); _pickerSelected = v::null; updateSelected(); return true; } return false; } DocumentData *EmojiListWidget::lookupCustomEmoji( const OverEmoji *over) const { return over ? lookupCustomEmoji(over->index, over->section) : nullptr; } DocumentData *EmojiListWidget::lookupCustomEmoji( int index, int section) const { if (_searchMode) { if (index < _searchResults.size()) { const auto document = std::get_if( &_searchResults[index].id.data); if (document) { return session().data().document(document->id); } } return nullptr; } else if (section == int(Section::Recent) && index < _recent.size()) { const auto document = std::get_if( &_recent[index].id.data); if (document) { return session().data().document(document->id); } } else if (section >= _staticCount && index < _custom[section - _staticCount].list.size()) { auto &set = _custom[section - _staticCount]; return set.list[index].document; } return nullptr; } EmojiPtr EmojiListWidget::lookupOverEmoji(const OverEmoji *over) const { const auto section = over ? over->section : -1; const auto index = over ? over->index : -1; return _searchMode ? ((index < _searchResults.size() && v::is(_searchResults[index].id.data)) ? v::get(_searchResults[index].id.data) : nullptr) : (section == int(Section::Recent) && index < _recent.size() && v::is(_recent[index].id.data)) ? v::get(_recent[index].id.data) : (section > int(Section::Recent) && section < _staticCount && index < _emoji[section].size()) ? _emoji[section][index] : nullptr; } EmojiChosen EmojiListWidget::lookupChosen( EmojiPtr emoji, not_null over) { const auto rect = emojiRect(over->section, over->index); const auto size = st::emojiStatusDefault.size(); const auto icon = QRect( rect.x() + (_singleSize.width() - size.width()) / 2, rect.y() + (_singleSize.height() - size.height()) / 2, rect.width(), rect.height()); return { .emoji = emoji, .messageSendingFrom = { .type = Ui::MessageSendingAnimationFrom::Type::Emoji, .globalStartGeometry = mapToGlobal(icon), }, }; } FileChosen EmojiListWidget::lookupChosen( not_null custom, const OverEmoji *over, Api::SendOptions options) { _grabbingChosen = true; const auto guard = gsl::finally([&] { _grabbingChosen = false; }); const auto rect = over ? emojiRect(over->section, over->index) : QRect(); const auto emoji = over ? QRect( rect.topLeft() + _areaPosition + _innerPosition + _customPosition, QSize(_customSingleSize, _customSingleSize) ) : QRect(); return { .document = custom, .options = options, .messageSendingFrom = { .type = Ui::MessageSendingAnimationFrom::Type::Emoji, .globalStartGeometry = over ? mapToGlobal(emoji) : QRect(), .frame = over ? Ui::GrabWidgetToImage(this, emoji) : QImage(), }, }; } void EmojiListWidget::mousePressEvent(QMouseEvent *e) { _lastMousePos = e->globalPos(); updateSelected(); if (checkPickerHide() || e->button() != Qt::LeftButton) { return; } setPressed(_selected); if (const auto over = std::get_if(&_selected)) { const auto emoji = lookupOverEmoji(over); if (emoji && emoji->hasVariants()) { _pickerSelected = _selected; setCursor(style::cur_default); if (!Core::App().settings().hasChosenEmojiVariant(emoji)) { showPicker(); } else { _previewTimer.cancel(); _showPickerTimer.callOnce(kColorPickerDelay); } } else if (lookupCustomEmoji(over)) { _showPickerTimer.cancel(); _previewTimer.callOnce(QApplication::startDragTime()); } } } void EmojiListWidget::mouseReleaseEvent(QMouseEvent *e) { _previewTimer.cancel(); auto pressed = _pressed; setPressed(v::null); _lastMousePos = e->globalPos(); if (!_picker->isHidden()) { if (_picker->rect().contains(_picker->mapFromGlobal(_lastMousePos))) { return _picker->handleMouseRelease(QCursor::pos()); } else if (const auto over = std::get_if(&_pickerSelected)) { const auto emoji = lookupOverEmoji(over); if (emoji && emoji->hasVariants() && Core::App().settings().hasChosenEmojiVariant(emoji)) { _picker->hideAnimated(); _pickerSelected = v::null; } } } updateSelected(); if (_showPickerTimer.isActive()) { _showPickerTimer.cancel(); _pickerSelected = v::null; _picker->hide(); } if (_previewShown) { _previewShown = false; return; } else if (v::is_null(_selected) || _selected != pressed) { return; } if (const auto over = std::get_if(&_selected)) { const auto section = over->section; const auto index = over->index; if (section >= _staticCount && sectionInfo(section).collapsed && index + 1 == _columnCount * kCollapsedRows) { _custom[section - _staticCount].expanded = true; resizeToWidth(width()); update(); return; } else if (const auto emoji = lookupOverEmoji(over)) { if (emoji->hasVariants() && !_picker->isHidden()) { return; } selectEmoji(lookupChosen(emoji, over)); } else if (const auto custom = lookupCustomEmoji(over)) { selectCustom(lookupChosen(custom, over)); } } else if (const auto set = std::get_if(&pressed)) { Assert(set->section >= _staticCount && set->section < _staticCount + _custom.size()); displaySet(_custom[set->section - _staticCount].id); } else if (auto button = std::get_if(&pressed)) { Assert(hasButton(button->section)); const auto id = hasColorButton(button->section) ? 0 : _custom[button->section - _staticCount].id; const auto usage = ChatHelpers::WindowUsage::PremiumPromo; if (hasColorButton(button->section)) { _pickerSelected = pressed; showPicker(); } else if (hasRemoveButton(button->section)) { removeSet(id); } else if (hasAddButton(button->section)) { _localSetsManager->install(id); } else if (const auto resolved = _show->resolveWindow(usage)) { _jumpedToPremium.fire({}); switch (_mode) { case Mode::Full: case Mode::UserpicBuilder: Settings::ShowPremium(resolved, u"animated_emoji"_q); break; case Mode::FullReactions: case Mode::RecentReactions: Settings::ShowPremium(resolved, u"infinite_reactions"_q); break; case Mode::EmojiStatus: case Mode::ChannelStatus: Settings::ShowPremium(resolved, u"emoji_status"_q); break; case Mode::TopicIcon: Settings::ShowPremium(resolved, u"forum_topic_icon"_q); break; case Mode::BackgroundEmoji: Settings::ShowPremium(resolved, u"name_color"_q); break; } } } } void EmojiListWidget::displaySet(uint64 setId) { if (setId == Data::Stickers::MegagroupSetId) { if (_megagroupSet->mgInfo->emojiSet.id) { setId = _megagroupSet->mgInfo->emojiSet.id; } else { return; } } const auto &sets = session().data().stickers().sets(); auto it = sets.find(setId); if (it != sets.cend()) { checkHideWithBox(Box(_show, it->second.get())); } } void EmojiListWidget::removeMegagroupSet(bool locally) { if (locally) { session().settings().setGroupEmojiSectionHidden(_megagroupSet->id); session().saveSettings(); refreshCustom(); return; } checkHideWithBox(Ui::MakeConfirmBox({ .text = tr::lng_emoji_remove_group_set(), .confirmed = crl::guard(this, [this, group = _megagroupSet]( Fn &&close) { Expects(group->mgInfo != nullptr); if (group->mgInfo->emojiSet) { session().api().setGroupEmojiSet(group, {}); } close(); }), .cancelled = [](Fn &&close) { close(); }, .labelStyle = &st().boxLabel, })); } void EmojiListWidget::removeSet(uint64 setId) { const auto &labelSt = st().boxLabel; if (setId == Data::Stickers::MegagroupSetId) { const auto i = ranges::find(_custom, setId, &CustomSet::id); Assert(i != end(_custom)); const auto removeLocally = !_megagroupSet->canEditEmoji(); removeMegagroupSet(removeLocally); } else if (auto box = MakeConfirmRemoveSetBox(&session(), labelSt, setId)) { checkHideWithBox(std::move(box)); } } void EmojiListWidget::selectEmoji(EmojiChosen data) { Core::App().settings().incrementRecentEmoji({ data.emoji }); _chosen.fire(std::move(data)); } void EmojiListWidget::selectCustom(FileChosen data) { const auto document = data.document; const auto skip = (document->isPremiumEmoji() && !session().premium()); if (!skip && _mode == Mode::Full) { auto &settings = Core::App().settings(); settings.incrementRecentEmoji({ RecentEmojiDocument{ document->id, document->session().isTestMode(), } }); } _customChosen.fire(std::move(data)); } void EmojiListWidget::showPicker() { if (v::is_null(_pickerSelected)) { return; } const auto showAt = [&](float64 xCoef, int y, int height) { y -= _picker->height() - st::emojiPanRadius + getVisibleTop(); if (y < st().header) { y += _picker->height() + height; } auto xmax = width() - _picker->width(); if (rtl()) xCoef = 1. - xCoef; _picker->move(qRound(xmax * xCoef), y); disableScroll(true); }; if (const auto button = std::get_if(&_pickerSelected)) { const auto hand = QString::fromUtf8("\xF0\x9F\x91\x8B"); const auto emoji = Ui::Emoji::Find(hand); Assert(emoji != nullptr && emoji->hasVariants()); _picker->showEmoji(emoji, true); setColorAllForceRippled(true); const auto rect = buttonRect(button->section); showAt(1., rect.y(), rect.height() - 2 * st::emojiPanRadius); } else if (const auto over = std::get_if(&_pickerSelected)) { const auto emoji = lookupOverEmoji(over); if (emoji && emoji->hasVariants()) { _picker->showEmoji(emoji); const auto coef = float64(over->index % _columnCount) / float64(_columnCount - 1); const auto h = _singleSize.height() - 2 * st::emojiPanRadius; showAt(coef, emojiRect(over->section, over->index).y(), h); } } } void EmojiListWidget::pickerHidden() { _pickerSelected = v::null; update(); disableScroll(false); setColorAllForceRippled(false); _lastMousePos = QCursor::pos(); updateSelected(); } bool EmojiListWidget::hasColorButton(int index) const { return (_staticCount > int(Section::People)) && (index == int(Section::People)); } QRect EmojiListWidget::colorButtonRect(int index) const { return colorButtonRect(sectionInfo(index)); } QRect EmojiListWidget::colorButtonRect(const SectionInfo &info) const { if (_mode != Mode::Full) { return QRect(); } const auto &colorSt = st().colorAll; const auto buttonw = colorSt.rippleAreaPosition.x() + colorSt.rippleAreaSize; const auto buttonh = colorSt.height; const auto buttonx = emojiRight() - st::emojiPanColorAllSkip - buttonw; const auto buttony = info.top + st::emojiPanRemoveTop; return QRect(buttonx, buttony, buttonw, buttonh); } bool EmojiListWidget::hasRemoveButton(int index) const { if (index < _staticCount || index >= _staticCount + _custom.size()) { return false; } const auto &set = _custom[index - _staticCount]; if (set.id == Data::Stickers::MegagroupSetId) { Assert(_megagroupSet != nullptr); if (index + 1 != _staticCount + _custom.size()) { return true; } return !set.list.empty() && _megagroupSet->canEditEmoji(); } return set.canRemove && !set.premiumRequired; } QRect EmojiListWidget::removeButtonRect(int index) const { return removeButtonRect(sectionInfo(index)); } QRect EmojiListWidget::removeButtonRect(const SectionInfo &info) const { if (_mode != Mode::Full) { return QRect(); } const auto &removeSt = st().removeSet; const auto buttonw = removeSt.rippleAreaPosition.x() + removeSt.rippleAreaSize; const auto buttonh = removeSt.height; const auto buttonx = emojiRight() - st::emojiPanRemoveSkip - buttonw; const auto buttony = info.top + st::emojiPanRemoveTop; return QRect(buttonx, buttony, buttonw, buttonh); } bool EmojiListWidget::hasAddButton(int index) const { if (index < _staticCount || index >= _staticCount + _custom.size()) { return false; } const auto &set = _custom[index - _staticCount]; return !set.canRemove && !set.premiumRequired && set.id != Data::Stickers::MegagroupSetId; } QRect EmojiListWidget::addButtonRect(int index) const { return buttonRect(sectionInfo(index), _add); } bool EmojiListWidget::hasUnlockButton(int index) const { if (index < _staticCount || index >= _staticCount + _custom.size()) { return false; } const auto &set = _custom[index - _staticCount]; return set.premiumRequired; } QRect EmojiListWidget::unlockButtonRect(int index) const { Expects(index >= _staticCount && index < _staticCount + _custom.size()); return buttonRect(sectionInfo(index), rightButton(index)); } bool EmojiListWidget::hasButton(int index) const { if (hasColorButton(index)) { return true; } else if (index >= _staticCount && index < _staticCount + _custom.size()) { const auto &custom = _custom[index - _staticCount]; return (custom.id != Data::Stickers::MegagroupSetId) || custom.canRemove; } return false; } QRect EmojiListWidget::buttonRect(int index) const { return hasColorButton(index) ? colorButtonRect(index) : hasRemoveButton(index) ? removeButtonRect(index) : hasAddButton(index) ? addButtonRect(index) : unlockButtonRect(index); } QRect EmojiListWidget::buttonRect( const SectionInfo &info, const RightButton &button) const { const auto buttonw = button.textWidth - st::emojiPanButton.width; const auto buttonh = st::emojiPanButton.height; const auto buttonx = emojiRight() - buttonw - st::emojiPanButtonRight; const auto buttony = info.top + st::emojiPanButtonTop; return QRect(buttonx, buttony, buttonw, buttonh); } auto EmojiListWidget::rightButton(int index) const -> const RightButton & { Expects(index >= _staticCount && index < _staticCount + _custom.size()); return hasAddButton(index) ? _add : _custom[index - _staticCount].canRemove ? _restore : _unlock; } int EmojiListWidget::emojiRight() const { return emojiLeft() + (_columnCount * _singleSize.width()); } int EmojiListWidget::emojiLeft() const { return _rowsLeft; } QRect EmojiListWidget::emojiRect(int section, int index) const { Expects(_columnCount > 0); const auto info = sectionInfo(section); const auto countTillItem = (index - (index % _columnCount)); const auto rowsToSkip = (countTillItem / _columnCount) + ((countTillItem % _columnCount) ? 1 : 0); const auto x = _rowsLeft + ((index % _columnCount) * _singleSize.width()); const auto y = info.rowsTop + rowsToSkip * _singleSize.height(); return QRect(x, y, _singleSize.width(), _singleSize.height()); } void EmojiListWidget::colorChosen(EmojiChosen data) { Expects(data.emoji != nullptr && data.emoji->hasVariants()); const auto emoji = data.emoji; auto &settings = Core::App().settings(); if (const auto button = std::get_if(&_pickerSelected)) { settings.saveAllEmojiVariants(emoji); for (auto section = int(Section::People) ; section < _staticCount ; ++section) { for (auto &emoji : _emoji[section]) { emoji = settings.lookupEmojiVariant(emoji); } } update(); } else { settings.saveEmojiVariant(emoji); const auto over = std::get_if(&_pickerSelected); if (over && over->section > int(Section::Recent) && over->section < _staticCount && over->index < _emoji[over->section].size()) { _emoji[over->section][over->index] = emoji; rtlupdate(emojiRect(over->section, over->index)); } selectEmoji(data); } _picker->hideAnimated(); } void EmojiListWidget::mouseMoveEvent(QMouseEvent *e) { _lastMousePos = e->globalPos(); if (!_picker->isHidden()) { if (_picker->rect().contains(_picker->mapFromGlobal(_lastMousePos))) { return _picker->handleMouseMove(QCursor::pos()); } else { _picker->clearSelection(); } } updateSelected(); } void EmojiListWidget::leaveEventHook(QEvent *e) { clearSelection(); } void EmojiListWidget::leaveToChildEvent(QEvent *e, QWidget *child) { clearSelection(); } void EmojiListWidget::enterFromChildEvent(QEvent *e, QWidget *child) { _lastMousePos = QCursor::pos(); updateSelected(); } void EmojiListWidget::clearSelection() { setPressed(v::null); setSelected(v::null); _lastMousePos = mapToGlobal(QPoint(-10, -10)); } uint64 EmojiListWidget::currentSet(int yOffset) const { return sectionSetId(sectionInfoByOffset(yOffset).section); } void EmojiListWidget::setAllowWithoutPremium(bool allow) { if (_allowWithoutPremium == allow) { return; } _allowWithoutPremium = allow; refreshCustom(); resizeToWidth(width()); } void EmojiListWidget::showMegagroupSet(ChannelData *megagroup) { Expects(!megagroup || megagroup->isMegagroup()); if (_megagroupSet != megagroup) { _megagroupSet = megagroup; refreshCustom(); resizeToWidth(width()); } } QString EmojiListWidget::tooltipText() const { if (_mode != Mode::Full) { return {}; } const auto &replacements = Ui::Emoji::internal::GetAllReplacements(); const auto over = std::get_if(&_selected); if (const auto emoji = lookupOverEmoji(over)) { const auto text = emoji->original()->text(); // find the replacement belonging to the emoji const auto it = ranges::find_if(replacements, [&](const auto &one) { return text == Ui::Emoji::QStringFromUTF16(one.emoji); }); if (it != replacements.end()) { return Ui::Emoji::QStringFromUTF16(it->replacement); } } return {}; } QPoint EmojiListWidget::tooltipPos() const { return _lastMousePos; } bool EmojiListWidget::tooltipWindowActive() const { return Ui::AppInFocus() && Ui::InFocusChain(window()); } TabbedSelector::InnerFooter *EmojiListWidget::getFooter() const { return _footer; } void EmojiListWidget::processHideFinished() { if (!_picker->isHidden()) { _picker->hideFast(); _pickerSelected = v::null; } unloadAllCustom(); clearSelection(); } void EmojiListWidget::processPanelHideFinished() { unloadAllCustom(); if (_localSetsManager->clearInstalledLocally()) { refreshCustom(); } } void EmojiListWidget::refreshRecent() { if (_mode != Mode::Full) { return; } clearSelection(); fillRecent(); resizeToWidth(width()); update(); } void EmojiListWidget::refreshCustom() { if (_mode == Mode::RecentReactions) { return; } auto old = base::take(_custom); const auto session = &this->session(); const auto premiumPossible = session->premiumPossible(); const auto onlyUnicodeEmoji = _onlyUnicodeEmoji || !premiumPossible; const auto premiumMayBeBought = (!onlyUnicodeEmoji) && premiumPossible && !session->premium() && !_allowWithoutPremium; const auto owner = &session->data(); const auto &sets = owner->stickers().sets(); const auto push = [&](uint64 setId, bool installed) { const auto megagroup = _megagroupSet && (setId == Data::Stickers::MegagroupSetId); const auto lookupId = megagroup ? _megagroupSet->mgInfo->emojiSet.id : setId; if (!lookupId) { return; } else if (!megagroup && !_custom.empty() && _custom.front().id == Data::Stickers::MegagroupSetId && _megagroupSet->mgInfo->emojiSet.id == setId) { // Skip the set that is already added as a megagroup set. return; } else if (megagroup && ranges::contains(_custom, lookupId, &CustomSet::id)) { // Skip the set that is already added as a custom set. return; } auto it = sets.find(lookupId); if (it == sets.cend() || it->second->stickers.isEmpty() || (_mode == Mode::BackgroundEmoji && !it->second->textColor()) || (_mode == Mode::ChannelStatus && !it->second->channelStatus())) { return; } const auto canRemove = megagroup ? (_megagroupSet->canEditEmoji() || installed) : !!(it->second->flags & Data::StickersSetFlag::Installed); const auto sortAsInstalled = canRemove && (!(it->second->flags & Data::StickersSetFlag::Featured) || !_localSetsManager->isInstalledLocally(lookupId)); if (!megagroup && sortAsInstalled != installed) { return; } auto premium = false; const auto &list = it->second->stickers; const auto i = ranges::find(old, setId, &CustomSet::id); if (i != end(old)) { const auto valid = [&] { const auto count = int(list.size()); if (i->list.size() != count) { return false; } for (auto k = 0; k != count; ++k) { if (!premium && !megagroup && list[k]->isPremiumEmoji()) { premium = true; } if (i->list[k].document != list[k]) { return false; } } return true; }(); if (premium && onlyUnicodeEmoji) { return; } else if (valid) { i->thumbnailDocument = it->second->lookupThumbnailDocument(); const auto premiumRequired = premium && premiumMayBeBought; if (i->canRemove != canRemove || i->premiumRequired != premiumRequired) { i->canRemove = canRemove; i->premiumRequired = premiumRequired; i->ripple.reset(); } if (i->canRemove && !i->premiumRequired) { i->expanded = false; } _custom.push_back(std::move(*i)); return; } } auto set = std::vector(); set.reserve(list.size()); for (const auto document : list) { if (_restrictedCustomList.contains(document->id)) { continue; } else if (const auto sticker = document->sticker()) { set.push_back({ .custom = resolveCustomEmoji(document, lookupId), .document = document, .emoji = Ui::Emoji::Find(sticker->alt), }); if (!premium && !megagroup && document->isPremiumEmoji()) { premium = true; } } } if (premium && onlyUnicodeEmoji) { return; } _custom.push_back({ .id = setId, .set = it->second.get(), .thumbnailDocument = it->second->lookupThumbnailDocument(), .title = it->second->title, .list = std::move(set), .canRemove = canRemove, .premiumRequired = premium && premiumMayBeBought, }); }; refreshMegagroupStickers(push, GroupStickersPlace::Visible); for (const auto setId : owner->stickers().emojiSetsOrder()) { push(setId, true); } for (const auto setId : owner->stickers().featuredEmojiSetsOrder()) { push(setId, false); } refreshMegagroupStickers(push, GroupStickersPlace::Hidden); _footer->refreshIcons( fillIcons(), currentSet(getVisibleTop()), nullptr, ValidateIconAnimations::None); update(); } Fn EmojiListWidget::repaintCallback( DocumentId documentId, uint64 setId) { return [=] { repaintCustom(setId); if (_recentCustomIds.contains(documentId)) { repaintCustom(RecentEmojiSectionSetId()); } if (_searchCustomIds.contains(documentId)) { repaintCustom(SearchEmojiSectionSetId()); } }; } not_null EmojiListWidget::resolveCustomEmoji( not_null document, uint64 setId) { Expects(document->sticker() != nullptr); const auto documentId = document->id; const auto i = _customEmoji.find(documentId); const auto recentOnly = (i != end(_customEmoji)) && i->second.recentOnly; if (i != end(_customEmoji) && !recentOnly) { return i->second.emoji.get(); } auto instance = document->owner().customEmojiManager().create( document, repaintCallback(documentId, setId), Data::CustomEmojiManager::SizeTag::Large); if (recentOnly) { for (auto &recent : _recent) { if (recent.custom && recent.custom == i->second.emoji.get()) { recent.custom = instance.get(); } } i->second.emoji = std::move(instance); i->second.recentOnly = false; return i->second.emoji.get(); } return _customEmoji.emplace( documentId, CustomEmojiInstance{ .emoji = std::move(instance) } ).first->second.emoji.get(); } Ui::Text::CustomEmoji *EmojiListWidget::resolveCustomRecent( RecentEmojiId customId) { const auto &data = customId.data; if (const auto document = std::get_if(&data)) { return resolveCustomRecent(document->id); } else if (const auto emoji = std::get_if(&data)) { return nullptr; } Unexpected("Custom recent emoji id."); } not_null EmojiListWidget::resolveCustomRecent( DocumentId documentId) { const auto i = _customRecent.find(documentId); if (i != end(_customRecent)) { return i->second.get(); } const auto j = _customEmoji.find(documentId); if (j != end(_customEmoji)) { return j->second.emoji.get(); } auto repaint = repaintCallback(documentId, RecentEmojiSectionSetId()); if (_customRecentFactory) { return _customRecent.emplace( documentId, _customRecentFactory(documentId, std::move(repaint)) ).first->second.get(); } auto custom = session().data().customEmojiManager().create( documentId, std::move(repaint), Data::CustomEmojiManager::SizeTag::Large); return _customEmoji.emplace( documentId, CustomEmojiInstance{ .emoji = std::move(custom), .recentOnly = true } ).first->second.emoji.get(); } void EmojiListWidget::refreshMegagroupStickers( Fn push, GroupStickersPlace place) { if (!_features.megagroupSet || !_megagroupSet || !_megagroupSet->mgInfo->emojiSet) { return; } auto canEdit = _megagroupSet->canEditEmoji(); auto isShownHere = [place](bool hidden) { return (hidden == (place == GroupStickersPlace::Hidden)); }; auto hidden = session().settings().isGroupEmojiSectionHidden(_megagroupSet->id); auto removeHiddenForGroup = [this, &hidden] { if (hidden) { session().settings().removeGroupEmojiSectionHidden(_megagroupSet->id); session().saveSettings(); hidden = false; } }; if (canEdit && hidden) { removeHiddenForGroup(); } const auto &set = _megagroupSet->mgInfo->emojiSet; if (!set.id || !isShownHere(hidden)) { return; } push(Data::Stickers::MegagroupSetId, !hidden); if (!_custom.empty() && _custom.back().id == Data::Stickers::MegagroupSetId) { return; } else if (_megagroupSetIdRequested == set.id) { return; } _megagroupSetIdRequested = set.id; _api.request(MTPmessages_GetStickerSet( Data::InputStickerSet(set), MTP_int(0) // hash )).done([=](const MTPmessages_StickerSet &result) { result.match([&](const MTPDmessages_stickerSet &data) { if (const auto set = session().data().stickers().feedSetFull(data)) { refreshCustom(); if (set->id == _megagroupSetIdRequested) { _megagroupSetIdRequested = 0; } else { LOG(("API Error: Got different set.")); } } }, [](const MTPDmessages_stickerSetNotModified &) { LOG(("API Error: Unexpected messages.stickerSetNotModified.")); }); }).send(); } std::vector EmojiListWidget::fillIcons() { auto result = std::vector(); result.reserve(2 + _custom.size()); result.emplace_back(RecentEmojiSectionSetId()); if (_mode != Mode::Full) { } else if (_custom.empty()) { using Section = Ui::Emoji::Section; for (auto i = int(Section::People); i <= int(Section::Symbols); ++i) { result.emplace_back(EmojiSectionSetId(Section(i))); } } else { result.emplace_back(AllEmojiSectionSetId()); } const auto esize = StickersListFooter::IconFrameSize(); for (const auto &custom : _custom) { if (custom.id == Data::Stickers::MegagroupSetId) { result.emplace_back(Data::Stickers::MegagroupSetId); result.back().megagroup = _megagroupSet; continue; } const auto set = custom.set; result.emplace_back(set, custom.thumbnailDocument, esize, esize); } return result; } int EmojiListWidget::paintButtonGetWidth( QPainter &p, const SectionInfo &info, bool selected, QRect clip) const { if (!hasButton(info.section)) { return 0; } auto &ripple = (info.section >= _staticCount) ? _custom[info.section - _staticCount].ripple : _colorAllRipple; const auto colorAll = hasColorButton(info.section); if (colorAll || hasRemoveButton(info.section)) { const auto rect = colorAll ? colorButtonRect(info) : removeButtonRect(info); if (rect.isEmpty()) { return 0; } else if (rect.intersects(clip)) { const auto &bst = colorAll ? st().colorAll : st().removeSet; if (colorAll && _colorAllRippleForced) { selected = true; } if (ripple) { ripple->paint( p, rect.x() + bst.rippleAreaPosition.x(), rect.y() + bst.rippleAreaPosition.y(), width()); if (ripple->empty()) { ripple.reset(); } } const auto &icon = selected ? bst.iconOver : bst.icon; icon.paint( p, (rect.topLeft() + QPoint( rect.width() - icon.width(), rect.height() - icon.height()) / 2), width()); } return emojiRight() - rect.x(); } const auto canAdd = hasAddButton(info.section); const auto &button = rightButton(info.section); const auto rect = buttonRect(info, button); p.drawImage(rect.topLeft(), selected ? button.backOver : button.back); if (ripple) { const auto color = QColor(0, 0, 0, 36); ripple->paint(p, rect.x(), rect.y(), width(), &color); if (ripple->empty()) { ripple.reset(); } } p.setPen(!canAdd ? st::premiumButtonFg : selected ? st::emojiPanButton.textFgOver : st::emojiPanButton.textFg); p.setFont(st::emojiPanButton.font); p.drawText( rect.x() - (st::emojiPanButton.width / 2), (rect.y() + st::emojiPanButton.textTop + st::emojiPanButton.font->ascent), button.text); return emojiRight() - rect.x(); } void EmojiListWidget::paintEmptySearchResults(Painter &p) { Inner::paintEmptySearchResults( p, st::emojiEmpty, tr::lng_emoji_nothing_found(tr::now)); } bool EmojiListWidget::eventHook(QEvent *e) { if (e->type() == QEvent::ParentChange) { if (_picker->parentWidget() != parentWidget()) { _picker->setParent(parentWidget()); } _picker->raise(); } return Inner::eventHook(e); } void EmojiListWidget::updateSelected() { if (!v::is_null(_pressed) || !v::is_null(_pickerSelected)) { if (!_previewShown) { return; } } auto newSelected = OverState{ v::null }; auto p = mapFromGlobal(_lastMousePos); auto info = sectionInfoByOffset(p.y()); auto section = info.section; if (p.y() >= info.top && p.y() < info.rowsTop) { if (hasButton(section) && myrtlrect(buttonRect(section)).contains(p.x(), p.y())) { newSelected = OverButton{ section }; } else if (_features.openStickerSets && section >= _staticCount && _mode == Mode::Full) { newSelected = OverSet{ section }; } } else if (p.y() >= info.rowsTop && p.y() < info.rowsBottom) { auto sx = (rtl() ? width() - p.x() : p.x()) - _rowsLeft; if (sx >= 0 && sx < _columnCount * _singleSize.width()) { const auto index = qFloor((p.y() - info.rowsTop) / _singleSize.height()) * _columnCount + qFloor(sx / _singleSize.width()); if (index < info.count) { newSelected = OverEmoji{ .section = section, .index = index }; } } } setSelected(newSelected); } void EmojiListWidget::setSelected(OverState newSelected) { if (_selected == newSelected) { return; } setCursor(!v::is_null(newSelected) ? style::cur_pointer : style::cur_default); const auto updateSelected = [&] { if (const auto sticker = std::get_if(&_selected)) { rtlupdate(emojiRect(sticker->section, sticker->index)); } else if (const auto button = std::get_if(&_selected)) { rtlupdate(buttonRect(button->section)); } }; updateSelected(); _selected = newSelected; updateSelected(); const auto hasSelection = !v::is_null(_selected); if (hasSelection && Core::App().settings().suggestEmoji()) { Ui::Tooltip::Show(350, this); } setCursor(hasSelection ? style::cur_pointer : style::cur_default); if (hasSelection && !_picker->isHidden()) { if (_selected != _pickerSelected) { _picker->hideAnimated(); } else { _picker->showAnimated(); } } else if (_previewShown && _pressed != _selected) { if (const auto over = std::get_if(&_selected)) { if (const auto custom = lookupCustomEmoji(over)) { _pressed = _selected; _show->showMediaPreview(custom->stickerSetOrigin(), custom); } } } } void EmojiListWidget::setPressed(OverState newPressed) { if (auto button = std::get_if(&_pressed)) { Assert(hasColorButton(button->section) || (button->section >= _staticCount && button->section < _staticCount + _custom.size())); auto &ripple = (button->section >= _staticCount) ? _custom[button->section - _staticCount].ripple : _colorAllRipple; if (ripple) { ripple->lastStop(); } } _pressed = newPressed; if (auto button = std::get_if(&_pressed)) { Assert(hasColorButton(button->section) || (button->section >= _staticCount && button->section < _staticCount + _custom.size())); auto &ripple = (button->section >= _staticCount) ? _custom[button->section - _staticCount].ripple : _colorAllRipple; if (!ripple) { ripple = createButtonRipple(button->section); } ripple->add(mapFromGlobal(QCursor::pos()) - buttonRippleTopLeft(button->section)); } } void EmojiListWidget::initButton( RightButton &button, const QString &text, bool gradient) { button.text = text; button.textWidth = st::emojiPanButton.font->width(text); const auto width = button.textWidth - st::emojiPanButton.width; const auto height = st::emojiPanButton.height; const auto factor = style::DevicePixelRatio(); auto prepare = [&](QColor bg, QBrush fg) { auto image = QImage( QSize(width, height) * factor, QImage::Format_ARGB32_Premultiplied); image.setDevicePixelRatio(factor); image.fill(Qt::transparent); auto p = QPainter(&image); auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(fg); const auto radius = height / 2.; p.drawRoundedRect(QRect(0, 0, width, height), radius, radius); p.end(); return image; }; button.back = prepare(Qt::transparent, [&]() -> QBrush { if (gradient) { auto result = QLinearGradient(QPointF(0, 0), QPointF(width, 0)); result.setStops(Ui::Premium::GiftGradientStops()); return result; } return st::emojiPanButton.textBg; }()); button.backOver = gradient ? button.back : prepare(Qt::transparent, st::emojiPanButton.textBgOver); button.rippleMask = prepare(Qt::black, Qt::white); } std::unique_ptr EmojiListWidget::createButtonRipple( int section) { Expects(hasButton(section)); const auto colorAll = hasColorButton(section); const auto remove = hasRemoveButton(section); const auto &staticSt = colorAll ? st().colorAll : st().removeSet; const auto &st = (colorAll || remove) ? staticSt.ripple : st::emojiPanButton.ripple; auto mask = (colorAll || remove) ? Ui::RippleAnimation::EllipseMask(QSize( staticSt.rippleAreaSize, staticSt.rippleAreaSize)) : rightButton(section).rippleMask; return std::make_unique( st, std::move(mask), [this, section] { rtlupdate(buttonRect(section)); }); } QPoint EmojiListWidget::buttonRippleTopLeft(int section) const { Expects(hasButton(section)); return myrtlrect(buttonRect(section)).topLeft() + (hasColorButton(section) ? st().colorAll.rippleAreaPosition : hasRemoveButton(section) ? st().removeSet.rippleAreaPosition : QPoint()); } PowerSaving::Flag EmojiListWidget::powerSavingFlag() const { const auto reactions = (_mode == Mode::FullReactions) || (_mode == Mode::RecentReactions); return reactions ? PowerSaving::kEmojiReactions : PowerSaving::kEmojiPanel; } void EmojiListWidget::refreshEmoji() { refreshRecent(); refreshCustom(); } void EmojiListWidget::showSet(uint64 setId) { clearSelection(); if (_search && _searchMode) { _search->cancel(); applyNextSearchQuery(); } auto y = 0; enumerateSections([&](const SectionInfo &info) { if (setId == sectionSetId(info.section)) { y = info.top; return false; } return true; }); scrollTo(y); _lastMousePos = QCursor::pos(); update(); } uint64 EmojiListWidget::sectionSetId(int section) const { Expects(_searchMode || section < _staticCount || (section - _staticCount) < _custom.size()); return _searchMode ? SearchEmojiSectionSetId() : (section < _staticCount) ? EmojiSectionSetId(static_cast
(section)) : _custom[section - _staticCount].id; } tr::phrase<> EmojiCategoryTitle(int index) { switch (index) { case 1: return tr::lng_emoji_category1; case 2: return tr::lng_emoji_category2; case 3: return tr::lng_emoji_category3; case 4: return tr::lng_emoji_category4; case 5: return tr::lng_emoji_category5; case 6: return tr::lng_emoji_category6; case 7: return tr::lng_emoji_category7; } Unexpected("Index in CategoryTitle."); } } // namespace ChatHelpers