diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 84516b7fd8..9335ad14ac 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -179,6 +179,8 @@ emojiSwitchColor: windowActiveTextFg; emojiSwitchStickers: icon {{ "emoji/emoji_switch", emojiSwitchColor }}; emojiSwitchEmoji: icon {{ "emoji/emoji_switch-flip_horizontal", emojiSwitchColor }}; +emojiIconSelectSkip: 2px; + hashtagClose: IconButton { width: 30px; height: 30px; diff --git a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp index 3f3714679b..9d915274f8 100644 --- a/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/emoji_list_widget.cpp @@ -1211,11 +1211,10 @@ void EmojiListWidget::refreshCustom() { std::vector EmojiListWidget::fillIcons() { auto result = std::vector(); - result.reserve(kEmojiSectionCount + _custom.size()); + result.reserve(2 + _custom.size()); - for (auto i = 0; i != kEmojiSectionCount; ++i) { - result.emplace_back(EmojiSectionSetId(static_cast
(i))); - } + result.emplace_back(EmojiSectionSetId(Ui::Emoji::Section::Recent)); + result.emplace_back(EmojiSectionSetId(Ui::Emoji::Section::People)); for (const auto &custom : _custom) { const auto set = custom.set; const auto s = custom.list[0].document; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp index e460f726aa..7634a3159c 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.cpp @@ -41,6 +41,14 @@ uint64 EmojiSectionSetId(EmojiSection section) { return kEmojiSectionSetIdBase + static_cast(section); } +uint64 RecentEmojiSectionSetId() { + return EmojiSectionSetId(EmojiSection::Recent); +} + +uint64 FirstEmojiSectionSetId() { + return EmojiSectionSetId(EmojiSection::People); +} + std::optional SetIdEmojiSection(uint64 id) { const auto base = EmojiSectionSetId(EmojiSection::Recent); if (id < base) { @@ -94,7 +102,10 @@ StickersListFooter::StickersListFooter(Descriptor &&descriptor) , _settingsButtonVisible(descriptor.settingsButtonVisible) , _iconsAnimation([=](crl::time now) { return iconsAnimationCallback(now); -}) { +}) +, _selectionBg(st::roundRadiusSmall, st::windowBgRipple) +, _emojiIconWidth(st::stickerIconWidth) +, _barSelection(descriptor.barSelection) { setMouseTracking(true); _iconsLeft = st::emojiCategorySkip + (_searchButtonVisible @@ -152,6 +163,7 @@ void StickersListFooter::clearHeavyData() { if (!info.visible) { icon.savedFrame = QPixmap(); } + return true; }); } @@ -224,39 +236,54 @@ void StickersListFooter::returnFocus() { } void StickersListFooter::enumerateVisibleIcons( - Fn callback) { - auto iconsX = qRound(_iconsX.current()); - auto x = _iconsLeft - (iconsX % st::stickerIconWidth); - auto first = floorclamp(iconsX, st::stickerIconWidth, 0, _icons.size()); - auto last = ceilclamp( - iconsX + width(), - st::stickerIconWidth, - 0, - _icons.size()); - for (auto index = first; index != last; ++index) { - callback({ .index = index, .left = x, .visible = true }); - x += st::stickerIconWidth; - } + Fn callback) const { + enumerateIcons([&](const IconInfo &info) { + if (info.visible) { + callback(info); + } else if (info.left > 0) { + return false; + } + return true; + }); } void StickersListFooter::enumerateIcons( - Fn callback) { - auto iconsX = qRound(_iconsX.current()); - auto x = _iconsLeft - (iconsX % st::stickerIconWidth); - auto first = floorclamp(iconsX, st::stickerIconWidth, 0, _icons.size()); - auto last = ceilclamp( - iconsX + width(), - st::stickerIconWidth, - 0, - _icons.size()); - x -= first * st::stickerIconWidth; + Fn callback) const { + auto iconsX = int(base::SafeRound(_iconsX.current())); + auto left = _iconsLeft - iconsX; + const auto emojiId = FirstEmojiSectionSetId(); + const auto right = width(); for (auto i = 0, count = int(_icons.size()); i != count; ++i) { - const auto visible = (i >= first && i < last); - callback({ .index = i, .left = x, .visible = visible }); - x += st::stickerIconWidth; + auto &icon = _icons[i]; + const auto width = (icon.setId == emojiId) + ? _emojiIconWidthAnimation.value(_emojiIconWidth) + : st::stickerIconWidth; + const auto visible = (left + width > 0 && left < right); + const auto result = callback({ + .index = i, + .left = left, + .width = int(base::SafeRound(width)), + .visible = visible, + }); + if (!result) { + break; + } + left += width; } } +auto StickersListFooter::iconInfo(int index) const -> IconInfo { + auto result = IconInfo(); + enumerateIcons([&](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]; @@ -276,8 +303,13 @@ void StickersListFooter::validateSelectedIcon( ValidateIconAnimations animations) { _activeByScrollId = setId; + using EmojiSection = Ui::Emoji::Section; auto favedIconIndex = -1; auto newSelected = -1; + const auto emojiSection = SetIdEmojiSection(setId); + const auto isEmojiSection = emojiSection.has_value() + && (emojiSection != EmojiSection::Recent); + const auto firstEmojiSetId = FirstEmojiSectionSetId(); for (auto i = 0, l = int(_icons.size()); i != l; ++i) { if (_icons[i].setId == setId || (_icons[i].setId == Data::Stickers::FavedSetId @@ -286,6 +318,8 @@ void StickersListFooter::validateSelectedIcon( break; } else if (_icons[i].setId == Data::Stickers::FavedSetId) { favedIconIndex = i; + } else if (isEmojiSection && _icons[i].setId == firstEmojiSetId) { + newSelected = i; } } setSelectedIcon( @@ -295,6 +329,39 @@ void StickersListFooter::validateSelectedIcon( animations); } +void StickersListFooter::updateEmojiSectionWidth() { + _emojiIconExpanded = (_iconSel >= 0) + && (_iconSel < _icons.size()) + && (_icons[_iconSel].setId == FirstEmojiSectionSetId()); + const auto desired = _emojiIconExpanded + ? (9 * st::stickerIconWidth / 2) + : st::stickerIconWidth; + if (_emojiIconWidth == desired) { + return; + } + _emojiIconWidthAnimation.start( + [=] { updateEmojiWidthCallback(); }, + _emojiIconWidth, + desired, + st::stickerIconMove); + _emojiIconWidth = desired; +} + +void StickersListFooter::updateEmojiWidthCallback() { + update(); + const auto info = iconInfo(_iconSel); + if (_iconSelX.from() != _iconSelX.to()) { + _iconSelX = anim::value(_iconSelX.from(), info.left); + } else { + _iconSelX = anim::value(info.left, info.left); + } + if (_iconSelWidth.from() != _iconSelWidth.to()) { + _iconSelWidth = anim::value(_iconSelWidth.from(), info.width); + } else { + _iconSelWidth = anim::value(info.width, info.width); + } +} + void StickersListFooter::setSelectedIcon( int newSelected, ValidateIconAnimations animations) { @@ -302,16 +369,18 @@ void StickersListFooter::setSelectedIcon( return; } _iconSel = newSelected; - auto iconSelXFinal = _iconSel * st::stickerIconWidth; + const auto info = iconInfo(_iconSel); + updateEmojiSectionWidth(); if (animations == ValidateIconAnimations::Full) { - _iconSelX.start(iconSelXFinal); + _iconSelX.start(info.left); + _iconSelWidth.start(info.width); } else { - _iconSelX = anim::value(iconSelXFinal, iconSelXFinal); + _iconSelX = anim::value(info.left, info.left); + _iconSelWidth = anim::value(info.width, info.width); } - auto iconsCountForCentering = (2 * _iconSel + 1); - auto iconsWidthForCentering = iconsCountForCentering - * st::stickerIconWidth; - auto iconsXFinal = std::clamp( + const auto relativeLeft = info.left - _iconsLeft; + const auto iconsWidthForCentering = 2 * relativeLeft + info.width; + const auto iconsXFinal = std::clamp( (_iconsLeft + iconsWidthForCentering + _iconsRight - width()) / 2, 0, _iconsMax); @@ -333,6 +402,7 @@ void StickersListFooter::processHideFinished() { _iconsAnimation.stop(); _iconsX.finish(); _iconSelX.finish(); + _iconSelWidth.finish(); _horizontal = false; } @@ -371,6 +441,10 @@ void StickersListFooter::paintEvent(QPaintEvent *e) { } p.setClipRect(clip); + if (!_barSelection) { + paintSelectionBg(p); + } + const auto now = crl::now(); const auto paused = _controller->isGifPausedAtLeastFor( Window::GifPauseReason::SavedGifs); @@ -378,20 +452,39 @@ void StickersListFooter::paintEvent(QPaintEvent *e) { paintSetIcon(p, info, now, paused); }); - paintSelectionBar(p); + if (_barSelection) { + paintSelectionBar(p); + } paintLeftRightFading(p); } -void StickersListFooter::paintSelectionBar(Painter &p) const { - auto selxrel = _iconsLeft + qRound(_iconSelX.current()); +void StickersListFooter::paintSelectionBg(Painter &p) const { + auto selxrel = qRound(_iconSelX.current()); auto selx = selxrel - qRound(_iconsX.current()); + const auto selw = qRound(_iconSelWidth.current()); if (rtl()) { - selx = width() - selx - st::stickerIconWidth; + selx = width() - selx - selw; + } + const auto skip = st::emojiIconSelectSkip; + const auto sely = _iconsTop + + (st::emojiFooterHeight - st::stickerIconWidth) / 2; + const auto selh = st::stickerIconWidth; + const auto rect = QRect(selx, sely, selw, selh); + const auto fill = rect.marginsRemoved({ skip, skip, skip, skip }); + _selectionBg.paint(p, fill); +} + +void StickersListFooter::paintSelectionBar(Painter &p) const { + auto selxrel = qRound(_iconSelX.current()); + auto selx = selxrel - qRound(_iconsX.current()); + const auto selw = qRound(_iconSelWidth.current()); + if (rtl()) { + selx = width() - selx - selw; } p.fillRect( selx, _iconsTop + st::emojiFooterHeight - st::stickerIconPadding, - st::stickerIconWidth, + selw, st::stickerIconSel, st::stickerIconSelColor); } @@ -509,9 +602,9 @@ void StickersListFooter::mouseReleaseEvent(QMouseEvent *e) { updateSelected(); if (wasDown == _iconOver) { if (const auto index = std::get_if(&_iconOver)) { - _iconSelX = anim::value( - *index * st::stickerIconWidth, - *index * st::stickerIconWidth); + const auto info = iconInfo(*index); + _iconSelX = anim::value(info.left, info.left); + _iconSelWidth = anim::value(info.width, info.width); _setChosen.fire_copy(_icons[*index].setId); } } @@ -585,7 +678,7 @@ void StickersListFooter::clipCallback( enumerateIcons([&](const IconInfo &info) { auto &icon = _icons[info.index]; if (icon.setId != setId || !icon.webm) { - return; + return true; } else if (icon.webm->state() == State::Error) { icon.webm.setBad(); } else if (!info.visible) { @@ -597,6 +690,7 @@ void StickersListFooter::clipCallback( }); } updateSetIconAt(info.left); + return true; }); } break; @@ -636,10 +730,14 @@ void StickersListFooter::updateSelected() { && y < _iconsTop + st::emojiFooterHeight && x >= _iconsLeft && x < width() - _iconsRight) { - x += qRound(_iconsX.current()) - _iconsLeft; - if (x < _icons.size() * st::stickerIconWidth) { - newOver = qFloor(x / st::stickerIconWidth); - } + x += qRound(_iconsX.current()); + enumerateIcons([&](const IconInfo &info) { + if (x >= info.left && x < info.left + info.width) { + newOver = info.index; + return false; + } + return true; + }); } } if (newOver != _iconOver) { @@ -698,10 +796,12 @@ void StickersListFooter::refreshIconsGeometry( _iconOver = _iconDown = SpecialOver::None; _iconsX.finish(); _iconSelX.finish(); + _iconSelWidth.finish(); _iconsStartAnim = 0; _iconsAnimation.stop(); + const auto &last = iconInfo(_icons.size() - 1); _iconsMax = std::max( - _iconsLeft + int(_icons.size()) * st::stickerIconWidth + _iconsRight - width(), + last.left + last.width + _iconsRight - width(), 0); if (_iconsX.current() > _iconsMax) { _iconsX = anim::value(_iconsMax, _iconsMax); @@ -880,35 +980,58 @@ void StickersListFooter::paintSetIcon( _iconsTop + (st::emojiFooterHeight - size.height()) / 2, _premiumIcon); } else { - const auto paintedIcon = [&] { - if (icon.setId == Data::Stickers::FeaturedSetId) { - const auto session = &_controller->session(); - return session->data().stickers().featuredSetsUnreadCount() - ? &st::stickersTrendingUnread - : &st::stickersTrending; - //} else if (setId == Stickers::FavedSetId) { - // return &st::stickersFaved; - } else if (const auto section = SetIdEmojiSection(icon.setId)) { - using Section = Ui::Emoji::Section; - switch (*section) { - case Section::Recent: return &st::emojiRecent; - case Section::People: return &st::emojiPeople; - case Section::Nature: return &st::emojiNature; - case Section::Food: return &st::emojiFood; - case Section::Activity: return &st::emojiActivity; - case Section::Travel: return &st::emojiTravel; - case Section::Objects: return &st::emojiObjects; - case Section::Symbols: return &st::emojiSymbols; - } - Unexpected("Section in SetIdEmojiSection result."); + using Section = Ui::Emoji::Section; + const auto sectionIcon = [&](Section section) { + switch (section) { + case Section::Recent: return &st::emojiRecent; + case Section::People: return &st::emojiPeople; + case Section::Nature: return &st::emojiNature; + case Section::Food: return &st::emojiFood; + case Section::Activity: return &st::emojiActivity; + case Section::Travel: return &st::emojiTravel; + case Section::Objects: return &st::emojiObjects; + case Section::Symbols: return &st::emojiSymbols; } - return &st::emojiRecent; - }(); - paintedIcon->paint( - p, - info.left + (st::stickerIconWidth - paintedIcon->width()) / 2, - _iconsTop + (st::emojiFooterHeight - paintedIcon->height()) / 2, - width()); + Unexpected("Section in SetIdEmojiSection result."); + }; + auto left = info.left; + const auto paintOne = [&](const style::icon *icon) { + icon->paint( + p, + left + (st::stickerIconWidth - icon->width()) / 2, + _iconsTop + (st::emojiFooterHeight - icon->height()) / 2, + width()); + }; + if (_icons[info.index].setId == FirstEmojiSectionSetId() + && info.width > st::stickerIconWidth) { + const auto skip = st::emojiIconSelectSkip; + p.save(); + p.setClipRect( + left + skip, + _iconsTop, + info.width - 2 * skip, + st::emojiFooterHeight, + Qt::IntersectClip); + for (auto i = int(Section::People); i <= int(Section::Symbols); ++i) { + paintOne(sectionIcon(Section(i))); + left += st::stickerIconWidth; + } + p.restore(); + } else { + paintOne([&] { + if (icon.setId == Data::Stickers::FeaturedSetId) { + const auto session = &_controller->session(); + return session->data().stickers().featuredSetsUnreadCount() + ? &st::stickersTrendingUnread + : &st::stickersTrending; + //} else if (setId == Stickers::FavedSetId) { + // return &st::stickersFaved; + } else if (const auto section = SetIdEmojiSection(icon.setId)) { + return sectionIcon(*section); + } + return &st::emojiRecent; + }()); + } } } @@ -917,14 +1040,17 @@ bool StickersListFooter::iconsAnimationCallback(crl::time now) { now += st::stickerIconMove; } if (_iconsStartAnim) { - const auto dt = (now - _iconsStartAnim) / float64(st::stickerIconMove); + const auto dt = (now - _iconsStartAnim) + / float64(st::stickerIconMove); if (dt >= 1.) { _iconsStartAnim = 0; _iconsX.finish(); _iconSelX.finish(); + _iconSelWidth.finish(); } else { _iconsX.update(dt, anim::linear); _iconSelX.update(dt, anim::linear); + _iconSelWidth.update(dt, anim::linear); } } diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h index d5c1afded6..5641b7e6da 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_footer.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/clip/media_clip_reader.h" #include "chat_helpers/tabbed_selector.h" +#include "ui/round_rect.h" namespace Ui { class InputField; @@ -40,6 +41,8 @@ enum class ValidateIconAnimations { }; [[nodiscard]] uint64 EmojiSectionSetId(Ui::Emoji::Section section); +[[nodiscard]] uint64 RecentEmojiSectionSetId(); +[[nodiscard]] uint64 FirstEmojiSectionSetId(); [[nodiscard]] std::optional SetIdEmojiSection(uint64 id); struct StickerIcon { @@ -77,6 +80,7 @@ public: not_null parent; bool searchButtonVisible = false; bool settingsButtonVisible = false; + bool barSelection = false; }; explicit StickersListFooter(Descriptor &&descriptor); @@ -127,11 +131,13 @@ private: struct IconInfo { int index = 0; int left = 0; + int width = 0; bool visible = false; }; - void enumerateVisibleIcons(Fn callback); - void enumerateIcons(Fn callback); + void enumerateVisibleIcons(Fn callback) const; + void enumerateIcons(Fn callback) const; + [[nodiscard]] IconInfo iconInfo(int index) const; [[nodiscard]] std::shared_ptr getLottieRenderer(); bool iconsAnimationCallback(crl::time now); @@ -154,10 +160,14 @@ private: const IconInfo &info, crl::time now, bool paused) const; + void paintSelectionBg(Painter &p) const; void paintSelectionBar(Painter &p) const; void paintLeftRightFading(Painter &p) const; void validatePremiumIcon() const; + void updateEmojiSectionWidth(); + void updateEmojiWidthCallback(); + void initSearch(); void toggleSearch(bool visible); void resizeSearchControls(); @@ -189,8 +199,15 @@ private: int _iconsMax = 0; anim::value _iconsX; anim::value _iconSelX; + anim::value _iconSelWidth; crl::time _iconsStartAnim = 0; + Ui::RoundRect _selectionBg; + Ui::Animations::Simple _emojiIconWidthAnimation; + int _emojiIconWidth = 0; + bool _emojiIconExpanded = false; + bool _barSelection = false; + bool _horizontal = false; bool _searchShown = false; diff --git a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp index 4ce3697b03..aa42d58ed7 100644 --- a/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/stickers_list_widget.cpp @@ -252,6 +252,7 @@ object_ptr StickersListWidget::createFooter() { .parent = this, .searchButtonVisible = !_isMasks, .settingsButtonVisible = true, + .barSelection = true, }); _footer = result; @@ -303,7 +304,7 @@ void StickersListWidget::visibleTopBottomUpdated( } if (_footer) { _footer->validateSelectedIcon( - currentSet(top), + currentSet(visibleTop), ValidateIconAnimations::Full); } }