/* 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 "ui/effects/animations.h" #include "ui/widgets/buttons.h" #include "ui/widgets/shadow.h" #include "ui/text/custom_emoji_instance.h" #include "ui/emoji_config.h" #include "ui/ui_utility.h" #include "ui/cached_round_corners.h" #include "lang/lang_keys.h" #include "layout/layout_position.h" #include "data/data_session.h" #include "data/data_document.h" #include "data/stickers/data_stickers.h" #include "data/stickers/data_custom_emoji.h" #include "emoji_suggestions_data.h" #include "emoji_suggestions_helper.h" #include "main/main_session.h" #include "core/core_settings.h" #include "core/application.h" #include "window/window_session_controller.h" #include "facades.h" #include "styles/style_chat_helpers.h" namespace ChatHelpers { class EmojiColorPicker : public Ui::RpWidget { public: EmojiColorPicker(QWidget *parent); void showEmoji(EmojiPtr emoji); void clearSelection(); void handleMouseMove(QPoint globalPos); void handleMouseRelease(QPoint globalPos); void setSingleSize(QSize size); void showAnimated(); void hideAnimated(); void hideFast(); rpl::producer chosen() const; 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 animationCallback(); void updateSize(); void drawVariant(Painter &p, int variant); void updateSelected(); void setSelected(int newSelected); bool _ignoreShow = false; QVector _variants; int _selected = -1; int _pressedSel = -1; QPoint _lastMousePos; QSize _singleSize; bool _hiding = false; QPixmap _cache; Ui::Animations::Simple _a_opacity; rpl::event_stream _chosen; rpl::event_stream<> _hidden; }; class EmojiListWidget::Footer : public TabbedSelector::InnerFooter { public: Footer(not_null parent); void setCurrentSectionIcon(Section section); protected: void processPanelHideFinished() override; void resizeEvent(QResizeEvent *e) override; private: void setActiveSection(Section section); not_null _pan; std::array, kEmojiSectionCount> _sections; }; struct EmojiListWidget::CustomInstance { CustomInstance( std::unique_ptr loader, Fn, Ui::CustomEmoji::RepaintRequest)> repaintLater, Fn repaint); Ui::CustomEmoji::Instance emoji; Ui::CustomEmoji::Object object; }; EmojiListWidget::CustomInstance::CustomInstance( std::unique_ptr loader, Fn, Ui::CustomEmoji::RepaintRequest)> repaintLater, Fn repaint) : emoji( Ui::CustomEmoji::Loading(std::move(loader), Ui::CustomEmoji::Preview()), std::move(repaintLater)) , object(&emoji, std::move(repaint)) { } EmojiListWidget::Footer::Footer(not_null parent) : InnerFooter(parent) , _pan(parent) , _sections { { object_ptr(this, st::emojiCategoryRecent), object_ptr(this, st::emojiCategoryPeople), object_ptr(this, st::emojiCategoryNature), object_ptr(this, st::emojiCategoryFood), object_ptr(this, st::emojiCategoryActivity), object_ptr(this, st::emojiCategoryTravel), object_ptr(this, st::emojiCategoryObjects), object_ptr(this, st::emojiCategorySymbols), } } { for (auto i = 0; i != _sections.size(); ++i) { auto value = static_cast
(i); _sections[i]->setClickedCallback([=] { setActiveSection(value); }); } setCurrentSectionIcon(Section::Recent); } void EmojiListWidget::Footer::resizeEvent(QResizeEvent *e) { auto availableWidth = (width() - st::emojiCategorySkip * 2); auto buttonWidth = availableWidth / _sections.size(); auto buttonsWidth = buttonWidth * _sections.size(); auto left = (width() - buttonsWidth) / 2; for (auto &button : _sections) { button->resizeToWidth(buttonWidth); button->moveToLeft(left, 0); left += button->width(); } } void EmojiListWidget::Footer::processPanelHideFinished() { // Preserve panel state through visibility toggles. //setCurrentSectionIcon(Section::Recent); } void EmojiListWidget::Footer::setCurrentSectionIcon(Section section) { std::array overrides = { { &st::emojiRecentActive, &st::emojiPeopleActive, &st::emojiNatureActive, &st::emojiFoodActive, &st::emojiActivityActive, &st::emojiTravelActive, &st::emojiObjectsActive, &st::emojiSymbolsActive, } }; for (auto i = 0; i != _sections.size(); ++i) { _sections[i]->setIconOverride((section == static_cast
(i)) ? overrides[i] : nullptr); } } void EmojiListWidget::Footer::setActiveSection(Ui::Emoji::Section section) { _pan->showEmojiSection(section); } EmojiColorPicker::EmojiColorPicker(QWidget *parent) : RpWidget(parent) { setMouseTracking(true); } void EmojiColorPicker::showEmoji(EmojiPtr emoji) { if (!emoji || !emoji->hasVariants()) { return; } _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::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(); resize(width, height); update(); updateSelected(); } void EmojiColorPicker::paintEvent(QPaintEvent *e) { Painter p(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::defaultRoundShadow); Ui::FillRoundRect(p, inner, st::boxBg, Ui::BoxCorners); auto x = st::emojiPanMargins.left() + 2 * st::emojiColorsPadding + _singleSize.width(); if (rtl()) x = width() - x - st::emojiColorsSep; p.fillRect(x, st::emojiPanMargins.top() + st::emojiColorsPadding, st::emojiColorsSep, inner.height() - st::emojiColorsPadding * 2, st::emojiColorsSepColor); if (_variants.isEmpty()) return; 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(_variants[_selected]); } _ignoreShow = true; hideAnimated(); } void EmojiColorPicker::setSingleSize(QSize size) { _singleSize = size; 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 (_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()) { _cache = Ui::GrabWidget(this); clearSelection(); } _hiding = true; _a_opacity.start([this] { animationCallback(); }, 1., 0., st::emojiPanDuration); } void EmojiColorPicker::showAnimated() { if (_ignoreShow) return; if (!isHidden() && !_hiding) { return; } _hiding = false; if (_cache.isNull()) { _cache = Ui::GrabWidget(this); clearSelection(); } show(); _a_opacity.start([this] { animationCallback(); }, 0., 1., st::emojiPanDuration); } void EmojiColorPicker::clearSelection() { _pressedSel = -1; setSelected(-1); _lastMousePos = mapToGlobal(QPoint(-10, -10)); } void EmojiColorPicker::updateSelected() { auto newSelected = -1; auto p = mapFromGlobal(_lastMousePos); auto sx = rtl() ? (width() - p.x()) : p.x(), y = p.y() - st::emojiPanMargins.top() - 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; } auto updateSelectedRect = [this] { 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, _singleSize.width(), _singleSize.height()); }; updateSelectedRect(); _selected = newSelected; updateSelectedRect(); setCursor((_selected >= 0) ? style::cur_pointer : style::cur_default); } void EmojiColorPicker::drawVariant(Painter &p, int variant) { QPoint w(st::emojiPanMargins.left() + st::emojiColorsPadding + variant * _singleSize.width() + (variant ? 2 * st::emojiColorsPadding + st::emojiColorsSep : 0), st::emojiPanMargins.top() + st::emojiColorsPadding); if (variant == _selected) { QPoint tl(w); if (rtl()) tl.setX(width() - tl.x() - _singleSize.width()); Ui::FillRoundRect(p, QRect(tl, _singleSize), st::emojiPanHover, Ui::StickerHoverCorners); } const auto esize = Ui::Emoji::GetSizeLarge(); Ui::Emoji::Draw( p, _variants[variant], esize, w.x() + (_singleSize.width() - (esize / cIntRetinaFactor())) / 2, w.y() + (_singleSize.height() - (esize / cIntRetinaFactor())) / 2); } EmojiListWidget::EmojiListWidget( QWidget *parent, not_null controller) : Inner(parent, controller) , _picker(this) , _showPickerTimer([=] { showPicker(); }) , _repaintTimer([=] { invokeRepaints(); }) { setMouseTracking(true); setAttribute(Qt::WA_OpaquePaintEvent); _picker->hide(); _esize = Ui::Emoji::GetSizeLarge(); for (auto i = 0; i != kEmojiSectionCount; ++i) { const auto section = static_cast
(i); _counts[i] = (section == Section::Recent) ? int(Core::App().settings().recentEmoji().size()) : Ui::Emoji::GetSectionCount(section); } _picker->chosen( ) | rpl::start_with_next([=](EmojiPtr emoji) { colorChosen(emoji); }, lifetime()); _picker->hidden( ) | rpl::start_with_next([=] { pickerHidden(); }, lifetime()); controller->session().data().stickers().updated( ) | rpl::start_with_next([=] { refreshCustom(); resizeToWidth(width()); }, lifetime()); } EmojiListWidget::~EmojiListWidget() { base::take(_instances); base::take(_repaints); } void EmojiListWidget::repaintLater( uint64 setId, Ui::CustomEmoji::RepaintRequest request) { if (_instances.empty()) { return; } auto &repaint = _repaints[request.duration]; if (repaint.when < request.when) { repaint.when = request.when; } repaint.ids.emplace(setId); scheduleRepaintTimer(); } void EmojiListWidget::scheduleRepaintTimer() { if (_repaintTimerScheduled) { return; } _repaintTimerScheduled = true; Ui::PostponeCall(this, [=] { _repaintTimerScheduled = false; auto next = crl::time(); for (const auto &[duration, bunch] : _repaints) { if (!next || next > bunch.when) { next = bunch.when; } } if (next && (!_repaintNext || _repaintNext > next)) { const auto now = crl::now(); if (now >= next) { _repaintNext = 0; _repaintTimer.cancel(); invokeRepaints(); } else { _repaintNext = next; _repaintTimer.callOnce(next - now); } } }); } void EmojiListWidget::invokeRepaints() { _repaintNext = 0; auto ids = base::flat_set(); const auto now = crl::now(); for (auto i = begin(_repaints); i != end(_repaints);) { if (i->second.when > now) { ++i; continue; } if (ids.empty()) { ids = std::move(i->second.ids); } else { for (const auto id : i->second.ids) { ids.emplace(id); } } i = _repaints.erase(i); } repaintCustom([&](uint64 id) { return ids.contains(id); }); scheduleRepaintTimer(); } template void EmojiListWidget::repaintCustom(CheckId checkId) { enumerateSections([&](const SectionInfo &info) { if (info.section >= kEmojiSectionCount && checkId(_custom[info.section - kEmojiSectionCount].id)) { update( 0, info.rowsTop, width(), info.rowsBottom - info.rowsTop); } return true; }); } rpl::producer EmojiListWidget::chosen() const { return _chosen.events(); } auto EmojiListWidget::customChosen() const -> rpl::producer { return _customChosen.events(); } void EmojiListWidget::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { Inner::visibleTopBottomUpdated(visibleTop, visibleBottom); if (_footer) { _footer->setCurrentSectionIcon(currentSection(visibleTop)); } unloadNotSeenCustom(visibleTop, visibleBottom); } void EmojiListWidget::unloadNotSeenCustom( int visibleTop, int visibleBottom) { enumerateSections([&](const SectionInfo &info) { if (info.section < kEmojiSectionCount || (info.rowsBottom > visibleTop && info.rowsTop < visibleBottom)) { return true; } auto &custom = _custom[info.section - kEmojiSectionCount]; if (!custom.painted) { return true; } custom.painted = false; for (const auto &single : custom.list) { single.instance->object.unload(); } return true; }); } object_ptr EmojiListWidget::createFooter() { Expects(_footer == nullptr); auto result = object_ptr