From 56627ac693bb754678ee91bccdc04295eb6a9766 Mon Sep 17 00:00:00 2001 From: Vitaly Zdanevich Date: Wed, 24 Jun 2026 09:57:40 +0400 Subject: [PATCH] Add large sticker preview setting Let users opt in to larger sticker set previews and adaptive dialog sizing while keeping compact previews as the default. Depends on desktop-app/lib_ui#310 for recentering resized box layers after screen changes. Co-authored-by: OpenAI ChatGPT --- Telegram/Resources/langs/lang.strings | 1 + .../SourceFiles/boxes/sticker_set_box.cpp | 263 ++++++++++++++++-- Telegram/SourceFiles/boxes/sticker_set_box.h | 6 + .../chat_helpers/chat_helpers.style | 6 + Telegram/SourceFiles/core/core_settings.cpp | 6 +- Telegram/SourceFiles/core/core_settings.h | 15 +- .../settings/sections/settings_chat.cpp | 31 +++ 7 files changed, 302 insertions(+), 26 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index d68c988595224b..959abbb571c518 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -694,6 +694,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_settings_suggest_animated_emoji" = "Suggest animated emoji"; "lng_settings_suggest_by_emoji" = "Suggest popular stickers by emoji"; "lng_settings_loop_stickers" = "Loop animated stickers"; +"lng_settings_large_sticker_preview" = "Large sticker previews"; "lng_settings_large_emoji" = "Large emoji"; "lng_settings_send_enter" = "Send with Enter"; "lng_settings_send_ctrlenter" = "Send with Ctrl+Enter"; diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.cpp b/Telegram/SourceFiles/boxes/sticker_set_box.cpp index 7200f6431bf661..a36295b55eda82 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.cpp +++ b/Telegram/SourceFiles/boxes/sticker_set_box.cpp @@ -75,11 +75,14 @@ For license and copyright information please follow this link: #include #include +#include +#include #include namespace { constexpr auto kStickersPerRow = 5; +constexpr auto kStickerRowsVisible = 5; constexpr auto kEmojiPerRow = 8; constexpr auto kMinRepaintDelay = crl::time(33); constexpr auto kMinAfterScrollDelay = crl::time(33); @@ -91,6 +94,116 @@ using Data::StickersPack; using SetFlag = Data::StickersSetFlag; using TLStickerSet = MTPmessages_StickerSet; +[[nodiscard]] int PercentOf(int value, int percent) { + return (value * percent) / 100; +} + +[[nodiscard]] int ButtonsHeight(const style::Box &box) { + return box.buttonPadding.top() + + box.buttonHeight + + box.buttonPadding.bottom(); +} + +[[nodiscard]] QSize AvailableBoxSize(QWidget *outer) { + if (outer && !outer->size().isEmpty()) { + return outer->size(); + } + if (outer) { + if (const auto screen = outer->screen()) { + return screen->availableGeometry().size(); + } + } + if (const auto screen = QApplication::primaryScreen()) { + return screen->availableGeometry().size(); + } + return QSize(); +} + +[[nodiscard]] QSize StickerCellSize( + Data::StickersType type, + bool largePreview) { + return (type == Data::StickersType::Emoji) + ? (largePreview ? st::emojiSetLargeSize : st::emojiSetSize) + : (largePreview ? st::stickersLargeSize : st::stickersSize); +} + +[[nodiscard]] QMargins StickerSetPadding(Data::StickersType type) { + return (type == Data::StickersType::Emoji) + ? st::emojiSetPadding + : st::stickersPadding; +} + +[[nodiscard]] int StickersBoxWidthForColumns( + int columns, + Data::StickersType type, + bool largePreview) { + const auto padding = StickerSetPadding(type); + return padding.left() + + (columns * StickerCellSize(type, largePreview).width()) + + padding.right(); +} + +[[nodiscard]] int StickersBoxHeightForRows( + int rows, + Data::StickersType type, + bool largePreview) { + const auto padding = StickerSetPadding(type); + return padding.top() + + (rows * StickerCellSize(type, largePreview).height()) + + padding.bottom(); +} + +[[nodiscard]] int StickerSetBoxWidth( + Data::StickersType type, + QSize availableBox, + bool largePreview) { + if (!largePreview) { + return st::boxWideWidth; + } + return (type == Data::StickersType::Emoji) + ? st::emojiSetLargeBoxWidth + : (availableBox.width() + ? std::min( + availableBox.width(), + std::max( + PercentOf( + availableBox.width(), + st::stickersLargeWidthPercent), + StickersBoxWidthForColumns( + kStickersPerRow, + type, + largePreview))) + : StickersBoxWidthForColumns( + kStickersPerRow, + type, + largePreview)); +} + +[[nodiscard]] int StickerSetBoxMaxHeight( + Data::StickersType type, + QSize availableBox, + bool largePreview) { + if (!largePreview) { + return (type == Data::StickersType::Emoji) + ? st::emojiSetMaxHeight + : st::stickersMaxHeight; + } + return (type == Data::StickersType::Emoji) + ? st::emojiSetLargeMaxHeight + : (availableBox.height() + ? std::max( + 1, + PercentOf( + availableBox.height(), + st::stickersLargeHeightPercent) + - st::boxTitleHeight + - ButtonsHeight(st::layerBox)) + : StickersBoxHeightForRows( + kStickerRowsVisible, + type, + largePreview)); +} + [[nodiscard]] std::optional ComputeImageColor( const style::icon &lockIcon, const QImage &frame, @@ -287,7 +400,8 @@ class StickerSetBox::Inner final : public Ui::RpWidget { QWidget *parent, std::shared_ptr show, const StickerSetIdentifier &set, - Data::StickersType type); + Data::StickersType type, + bool largePreview); [[nodiscard]] bool loaded() const; [[nodiscard]] bool notInstalled() const; @@ -346,6 +460,7 @@ class StickerSetBox::Inner final : public Ui::RpWidget { void contextMenuEvent(QContextMenuEvent *e) override; void paintEvent(QPaintEvent *e) override; void leaveEventHook(QEvent *e) override; + void resizeEvent(QResizeEvent *e) override; private: struct Element { @@ -415,6 +530,8 @@ class StickerSetBox::Inner final : public Ui::RpWidget { [[nodiscard]] QPoint posFromIndex(int index) const; [[nodiscard]] bool isDraggedAnimating() const; + [[nodiscard]] int perRowForWidth(int newWidth) const; + void updateLayout(int newWidth); not_null getLottiePlayer(); @@ -473,6 +590,7 @@ class StickerSetBox::Inner final : public Ui::RpWidget { const std::unique_ptr _pathGradient; mutable StickerPremiumMark _premiumMark; + const bool _largePreview = false; int _visibleTop = 0; int _visibleBottom = 0; @@ -515,6 +633,7 @@ StickerSetBox::StickerSetBox( , _session(&_show->session()) , _set(set) , _type(type) +, _largePreview(Core::App().settings().largeStickerPreview()) , _previewDocumentId(previewDocumentId) { } @@ -548,9 +667,14 @@ void StickerSetBox::prepare() { setTitle(tr::lng_contacts_loading()); _inner = setInnerWidget( - object_ptr(this, _show, _set, _type), + object_ptr(this, _show, _set, _type, _largePreview), st::stickersScroll); - _inner->setOuterContainer(getDelegate()->outerContainer()); + const auto outer = getDelegate()->outerContainer(); + _inner->setOuterContainer(outer); + _adaptiveDimensionsTimer.setCallback([=] { + updateAdaptiveDimensions(); + }); + setupAdaptiveDimensionsWatcher(outer); if (const auto previewId = base::take(_previewDocumentId)) { _inner->showPreviewForDocument(previewId); } @@ -560,11 +684,7 @@ void StickerSetBox::prepare() { updateButtons(); }, lifetime()); - setDimensions( - st::boxWideWidth, - (_type == Data::StickersType::Emoji - ? st::emojiSetMaxHeight - : st::stickersMaxHeight)); + updateAdaptiveDimensions(); updateTitleAndButtons(); @@ -633,6 +753,52 @@ void StickerSetBox::prepare() { }, lifetime()); } +void StickerSetBox::updateAdaptiveDimensions() { + const auto outer = getDelegate()->outerContainer(); + const auto availableBox = AvailableBoxSize(outer.data()); + const auto size = QSize( + StickerSetBoxWidth(_type, availableBox, _largePreview), + StickerSetBoxMaxHeight(_type, availableBox, _largePreview)); + if (_lastAdaptiveBoxSize == size) { + return; + } + _lastAdaptiveBoxSize = size; + setDimensions(size.width(), size.height(), _largePreview); +} + +void StickerSetBox::scheduleAdaptiveDimensionsUpdate() { + _adaptiveDimensionsTimer.callOnce(0); +} + +void StickerSetBox::setupAdaptiveDimensionsWatcher(QPointer outer) { + if (!outer) { + return; + } + const auto filter = [=](not_null event) { + const auto type = event->type(); + if (type == QEvent::Move + || type == QEvent::Resize + || type == QEvent::Show + || type == QEvent::WindowStateChange + || type == QEvent::ScreenChangeInternal) { + scheduleAdaptiveDimensionsUpdate(); + } + return base::EventFilterResult::Continue; + }; + base::install_event_filter(this, outer.data(), filter); + + const auto window = outer->window(); + if (window && window != outer.data()) { + base::install_event_filter(this, window, filter); + } + const auto handle = window ? window->windowHandle() : nullptr; + if (handle) { + QObject::connect(handle, &QWindow::screenChanged, this, [=] { + scheduleAdaptiveDimensionsUpdate(); + }); + } +} + void StickerSetBox::addStickers() { _inner->install(); } @@ -898,9 +1064,12 @@ void StickerSetBox::updateButtons() { auto button = CreateUnlockButton( this, tr::lng_premium_unlock_emoji()); - button->resizeToWidth(st::boxWideWidth - - st.buttonPadding.left() - - st.buttonPadding.left()); + const auto availableBox = AvailableBoxSize( + getDelegate()->outerContainer().data()); + button->resizeToWidth( + StickerSetBoxWidth(type, availableBox, _largePreview) + - st.buttonPadding.left() + - st.buttonPadding.left()); button->setClickedCallback([=] { if (const auto window = _show->resolveWindow()) { Settings::ShowPremium(window, u"animated_emoji"_q); @@ -1047,7 +1216,8 @@ StickerSetBox::Inner::Inner( QWidget *parent, std::shared_ptr show, const StickerSetIdentifier &set, - Data::StickersType type) + Data::StickersType type, + bool largePreview) : RpWidget(parent) , _show(std::move(show)) , _session(&_show->session()) @@ -1060,11 +1230,10 @@ StickerSetBox::Inner::Inner( st::windowBgOver, [=] { repaintItems(); })) , _premiumMark(_session, st::stickersPremiumLock) +, _largePreview(largePreview) , _updateItemsTimer([=] { updateItems(); }) , _input(set) -, _padding((type == Data::StickersType::Emoji) - ? st::emojiSetPadding - : st::stickersPadding) +, _padding(StickerSetPadding(type)) , _previewTimer([=] { showPreview(); }) { setAttribute(Qt::WA_OpaquePaintEvent); @@ -1088,6 +1257,51 @@ StickerSetBox::Inner::Inner( setMouseTracking(true); } +void StickerSetBox::Inner::resizeEvent(QResizeEvent *e) { + RpWidget::resizeEvent(e); + updateLayout(width()); +} + +int StickerSetBox::Inner::perRowForWidth(int newWidth) const { + if (isEmojiSet()) { + return kEmojiPerRow; + } + if (!_largePreview) { + return kStickersPerRow; + } + if (_singleSize.width() <= 0 || newWidth <= 0) { + return kStickersPerRow; + } + const auto available = newWidth + - _padding.left() + - _padding.right(); + return std::max(1, available / _singleSize.width()); +} + +void StickerSetBox::Inner::updateLayout(int newWidth) { + if (!_loaded || _singleSize.isEmpty()) { + return; + } + const auto perRow = perRowForWidth(newWidth); + const auto rowsCount = (totalCellsCount() + perRow - 1) / perRow; + const auto newHeight = _padding.top() + + rowsCount * _singleSize.height() + + _padding.bottom(); + const auto changed = (_perRow != perRow) + || (_rowsCount != rowsCount) + || (height() != newHeight); + + _perRow = perRow; + _rowsCount = rowsCount; + if (height() != newHeight) { + resize(newWidth, newHeight); + } + if (changed) { + updateSelected(); + update(); + } +} + void StickerSetBox::Inner::applySet(const TLStickerSet &set) { _pack.clear(); _emoji.clear(); @@ -1189,12 +1403,15 @@ void StickerSetBox::Inner::applySet(const TLStickerSet &set) { return; } _loaded = true; - _perRow = isEmojiSet() ? kEmojiPerRow : kStickersPerRow; - _singleSize = isEmojiSet() ? st::emojiSetSize : st::stickersSize; + _singleSize = StickerCellSize(setType(), _largePreview); + _perRow = perRowForWidth(width()); _rowsCount = (totalCellsCount() + _perRow - 1) / _perRow; + const auto minWidth = _padding.left() + + _perRow * _singleSize.width() + + _padding.right(); resize( - _padding.left() + _perRow * _singleSize.width(), + std::max(width(), minWidth), _padding.top() + _rowsCount * _singleSize.height() + _padding.bottom()); if (const auto previewId = base::take(_previewDocumentId)) { @@ -2269,7 +2486,11 @@ void StickerSetBox::Inner::paintSticker( const auto document = element.document; const auto &media = element.documentMedia; const auto sticker = document->sticker(); - media->checkStickerSmall(); + if (_largePreview) { + media->checkStickerLarge(); + } else { + media->checkStickerSmall(); + } if (sticker->setType == Data::StickersType::Emoji) { const_cast(this)->setupEmoji(index); @@ -2308,7 +2529,9 @@ void StickerSetBox::Inner::paintSticker( .frame = size, .keepAlpha = true, }, paused ? 0 : now)); - } else if (const auto image = media->getStickerSmall()) { + } else if (const auto image = _largePreview + ? media->getStickerLarge() + : media->getStickerSmall()) { const auto pixmap = image->pix(size); p.drawPixmapLeft(ppos, width(), pixmap); if (premium) { diff --git a/Telegram/SourceFiles/boxes/sticker_set_box.h b/Telegram/SourceFiles/boxes/sticker_set_box.h index b4a88ce9306d69..4d8ae230b9a651 100644 --- a/Telegram/SourceFiles/boxes/sticker_set_box.h +++ b/Telegram/SourceFiles/boxes/sticker_set_box.h @@ -91,14 +91,20 @@ class StickerSetBox final : public Ui::BoxContent { void addStickers(); void copyStickersLink(); void handleError(Error error); + void updateAdaptiveDimensions(); + void scheduleAdaptiveDimensionsUpdate(); + void setupAdaptiveDimensionsWatcher(QPointer outer); const std::shared_ptr _show; const not_null _session; const StickerSetIdentifier _set; const Data::StickersType _type; + const bool _largePreview = false; class Inner; QPointer _inner; DocumentId _previewDocumentId = 0; + base::Timer _adaptiveDimensionsTimer; + QSize _lastAdaptiveBoxSize; }; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 14fd2ca8ec45b4..c40a770e88bd80 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -431,9 +431,15 @@ stickersFeaturedUnreadTop: 7px; stickersMaxHeight: 320px; stickersPadding: margins(19px, 13px, 19px, 13px); stickersSize: size(64px, 64px); +stickersLargeSize: size(224px, 224px); +stickersLargeWidthPercent: 95; +stickersLargeHeightPercent: 95; emojiSetPadding: margins(12px, 0px, 12px, 0px); emojiSetMaxHeight: 197px; +emojiSetLargeMaxHeight: 394px; +emojiSetLargeBoxWidth: 720px; emojiSetSize: size(42px, 39px); +emojiSetLargeSize: size(84px, 78px); stickersScroll: ScrollArea(boxScroll) { deltat: 19px; deltab: 9px; diff --git a/Telegram/SourceFiles/core/core_settings.cpp b/Telegram/SourceFiles/core/core_settings.cpp index 2ab79584859b6c..1561cc54792770 100644 --- a/Telegram/SourceFiles/core/core_settings.cpp +++ b/Telegram/SourceFiles/core/core_settings.cpp @@ -1222,13 +1222,15 @@ void Settings::writePrefGeneric( _saveDelayed.fire({}); } -std::optional Settings::readPrefGeneric(std::string_view key) { +std::optional Settings::readPrefGeneric( + std::string_view key) const { const auto i = _prefs.find(QByteArray(key.data(), key.size())); return (i != end(_prefs)) ? i->second : std::optional(); } template <> -std::optional Settings::readPrefImpl(std::string_view key) { +std::optional Settings::readPrefImpl( + std::string_view key) const { if (const auto data = readPrefGeneric(key)) { return !data->isEmpty(); } diff --git a/Telegram/SourceFiles/core/core_settings.h b/Telegram/SourceFiles/core/core_settings.h index 26da92fd8f6b14..6bfdf08f0aa7be 100644 --- a/Telegram/SourceFiles/core/core_settings.h +++ b/Telegram/SourceFiles/core/core_settings.h @@ -38,6 +38,8 @@ namespace Core { inline constexpr auto kScreenReaderModeDisabledKey = "screen-reader-mode-disabled"_cs; +inline constexpr auto kLargeStickerPreviewKey + = "large-sticker-preview"_cs; struct WindowPosition { int32 moncrc = 0; @@ -448,6 +450,12 @@ class Settings final { void setLoopAnimatedStickers(bool value) { _loopAnimatedStickers = value; } + [[nodiscard]] bool largeStickerPreview() const { + return readPref(kLargeStickerPreviewKey, false); + } + void setLargeStickerPreview(bool value) { + writePref(kLargeStickerPreviewKey, value); + } void setLargeEmoji(bool value) { _largeEmoji = value; } @@ -1034,7 +1042,7 @@ class Settings final { template [[nodiscard]] Type readPref( std::string_view key, - Other &&fallback = Type()) { + Other &&fallback = Type()) const { return readPrefImpl(key).value_or( std::forward(fallback)); } @@ -1048,11 +1056,11 @@ class Settings final { void writePrefImpl(std::string_view key, Type value); template - [[nodiscard]] std::optional readPrefImpl(std::string_view key); + [[nodiscard]] std::optional readPrefImpl(std::string_view key) const; void writePrefGeneric(std::string_view key, const QByteArray &value); [[nodiscard]] std::optional readPrefGeneric( - std::string_view key); + std::string_view key) const; static constexpr auto kDefaultThirdColumnWidth = 0; static constexpr auto kDefaultDialogsWidthRatio = 5. / 14; @@ -1207,4 +1215,3 @@ class Settings final { }; } // namespace Core - diff --git a/Telegram/SourceFiles/settings/sections/settings_chat.cpp b/Telegram/SourceFiles/settings/sections/settings_chat.cpp index 6ab0a4d2b7f78f..8050acfe7b8c1a 100644 --- a/Telegram/SourceFiles/settings/sections/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/sections/settings_chat.cpp @@ -966,6 +966,23 @@ void BuildStickersEmojiSection(SectionBuilder &builder) { }; }); + builder.add(nullptr, [] { + return SearchEntry{ + .id = u"chat/large-sticker-preview"_q, + .title = tr::lng_settings_large_sticker_preview(tr::now), + .keywords = { + u"large"_q, + u"big"_q, + u"stickers"_q, + u"preview"_q, + u"pack"_q, + }, + .checkIcon = Core::App().settings().largeStickerPreview() + ? SearchEntryCheckIcon::Checked + : SearchEntryCheckIcon::Unchecked, + }; + }); + builder.add(nullptr, [] { return SearchEntry{ .id = u"chat/replace-emoji"_q, @@ -1462,6 +1479,20 @@ void SetupStickersEmoji( } }); } + const auto largeStickerPreview = addWithReturn( + tr::lng_settings_large_sticker_preview(tr::now), + Core::App().settings().largeStickerPreview(), + [=](bool checked) { + Core::App().settings().setLargeStickerPreview(checked); + Core::App().saveSettingsDelayed(); + }); + if (highlights) { + highlights->push_back({ u"chat/large-sticker-preview"_q, { + largeStickerPreview, + { .radius = st::boxRadius } + } }); + } + const auto replaceEmoji = addWithReturn( tr::lng_settings_replace_emojis(tr::now), Core::App().settings().replaceEmoji(),