/* 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 "boxes/star_gift_preview_box.h" #include "base/random.h" #include "boxes/star_gift_box.h" #include "chat_helpers/stickers_lottie.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_document.h" #include "data/data_document_media.h" #include "data/data_session.h" #include "data/data_star_gift.h" #include "history/view/media/history_view_sticker_player.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "ui/layers/generic_box.h" #include "ui/text/text_custom_emoji.h" #include "ui/widgets/buttons.h" #include "ui/painter.h" #include "ui/top_background_gradient.h" #include "settings/settings_credits_graphics.h" #include "styles/style_credits.h" #include "styles/style_layers.h" namespace Ui { namespace { using Tab = Data::GiftAttributeIdType; using GiftModel = Data::UniqueGiftModel; using GiftPattern = Data::UniqueGiftPattern; using GiftBackdrop = Data::UniqueGiftBackdrop; constexpr auto kSwitchTimeout = 3 * crl::time(1000); constexpr auto kGiftsPerRow = 3; struct AttributeDescriptor : std::variant { friend inline bool operator==( const AttributeDescriptor &, const AttributeDescriptor &) = default; }; [[nodiscard]] DocumentData *Sticker(const AttributeDescriptor &value) { using namespace Data; if (const auto pattern = std::get_if(&value)) { return pattern->document; } else if (const auto model = std::get_if(&value)) { return model->document; } return nullptr; } struct BackdropPlayers { HistoryView::StickerPlayer *now = nullptr; HistoryView::StickerPlayer *next = nullptr; float64 progress = 0.; }; struct PatternEmoji { std::shared_ptr now; std::shared_ptr next; float64 progress = 0.; }; class AttributeDelegate { public: [[nodiscard]] virtual QSize buttonSize() = 0; [[nodiscard]] virtual QMargins buttonExtend() const = 0; [[nodiscard]] virtual QImage background() = 0; [[nodiscard]] virtual QImage cachePatternBackground( int width, int height, QMargins extend) = 0; [[nodiscard]] virtual QColor patternColor() = 0; [[nodiscard]] virtual BackdropPlayers backdropPlayers() = 0; [[nodiscard]] virtual PatternEmoji patternEmoji() = 0; }; class AttributeButton final : public Ui::AbstractButton { public: AttributeButton( QWidget *parent, not_null delegate, const AttributeDescriptor &descriptor); void toggleSelected(bool selected, anim::type animated); void setDescriptor(const AttributeDescriptor &descriptor); void setGeometry(QRect inner, QMargins extend); private: struct PatternCache { base::flat_map map; std::shared_ptr emoji; }; void paintEvent(QPaintEvent *e) override; void paintBackground(QPainter &p, const QImage &background); void setup(); void validatePatternCache(); void setDocument(not_null document); const not_null _delegate; QImage _hiddenBgCache; AttributeDescriptor _descriptor; Ui::Text::String _name; Ui::Text::String _percent; QImage _backgroundCache; PatternCache _patternNow; PatternCache _patternNext; QImage _patternFrame; QColor _patternColor; Ui::Animations::Simple _selectedAnimation; bool _selected : 1 = false; bool _patterned : 1 = false; QMargins _extend; DocumentData *_document = nullptr; DocumentData *_playerDocument = nullptr; std::unique_ptr _player; rpl::lifetime _mediaLifetime; }; class Delegate final : public AttributeDelegate { public: explicit Delegate(Fn fullUpdate); Delegate(Delegate &&other) = default; ~Delegate() = default; void update( const std::optional &now, const std::optional &next, float64 progress); [[nodiscard]] QSize buttonSize() override; [[nodiscard]] QMargins buttonExtend() const override; [[nodiscard]] QImage background() override; [[nodiscard]] QImage cachePatternBackground( int width, int height, QMargins extend) override; [[nodiscard]] QColor patternColor() override; [[nodiscard]] BackdropPlayers backdropPlayers() override; [[nodiscard]] PatternEmoji patternEmoji() override; private: struct Sticker { DocumentData *document = nullptr; DocumentData *playerDocument = nullptr; std::unique_ptr player; rpl::lifetime mediaLifetime; }; struct Emoji { DocumentData *document; std::shared_ptr custom; }; Fn _fullUpdate; QSize _single; QImage _bg; GiftBackdrop _nowBackdrop; GiftBackdrop _nextBackdrop; Sticker _nowModel; Sticker _nextModel; Emoji _nowPatternEmoji; Emoji _nextPatternEmoji; float64 _progress = 0.; GiftBackdrop _backdrop; QImage _backdropCache; bool _backdropDirty = false; }; struct Selection { int model = -1; int pattern = -1; int backdrop = -1; friend inline bool operator==(Selection, Selection) = default; }; class AttributesList final : public Ui::BoxContentDivider { public: AttributesList( QWidget *parent, not_null delegate, not_null attributes, rpl::producer tab, Selection initialSelection); [[nodiscard]] rpl::producer selected() const; private: struct Entry { AttributeDescriptor descriptor; Selection value; }; struct Entries { std::vector list; }; struct View { std::unique_ptr button; DocumentData *document = nullptr; int index = 0; }; void visibleTopBottomUpdated( int visibleTop, int visibleBottom) override; void paintEvent(QPaintEvent *e) override; void fill(); void refreshAbout(); void refreshButtons(); void validateButtons(); int resizeGetHeight(int width) override; void clicked(int index); const not_null _delegate; const not_null _attributes; rpl::variable _tab; rpl::variable _selected; std::unique_ptr _about; Entries _models; Entries _patterns; Entries _backdrops; not_null _entries; not_null*> _list; std::vector _views; int _viewsForWidth = 0; int _viewsFromRow = 0; int _viewsTillRow = 0; QSize _singleMin; QSize _single; int _perRow = 0; int _visibleFrom = 0; int _visibleTill = 0; }; void CacheBackdropBackground( not_null backdrop, int width, int height, QMargins extend, QImage &cache, bool force = false) { const auto outer = QRect(0, 0, width, height); const auto inner = outer.marginsRemoved( extend ).translated(-extend.left(), -extend.top()); const auto ratio = style::DevicePixelRatio(); if (cache.size() != inner.size() * ratio) { cache = QImage( inner.size() * ratio, QImage::Format_ARGB32_Premultiplied); cache.setDevicePixelRatio(ratio); force = true; } if (force) { cache.fill(Qt::transparent); const auto radius = st::giftBoxGiftRadius; auto p = QPainter(&cache); auto hq = PainterHighQualityEnabler(p); auto gradient = QRadialGradient(inner.center(), inner.width() / 2); gradient.setStops({ { 0., backdrop->centerColor }, { 1., backdrop->edgeColor }, }); p.setBrush(gradient); p.setPen(Qt::NoPen); p.drawRoundedRect(inner, radius, radius); } } AttributeButton::AttributeButton( QWidget *parent, not_null delegate, const AttributeDescriptor &descriptor) : AbstractButton(parent) , _delegate(delegate) , _descriptor(descriptor) { setup(); } void AttributeButton::setup() { v::match(_descriptor, [&](const auto &data) { _name.setText(st::uniqueAttributeName, data.name); _percent.setText( st::uniqueAttributePercent, QString::number(data.rarityPermille / 10.) + '%'); }); v::match(_descriptor, [&](const GiftBackdrop &data) { const auto destroyed = base::take(_player); _document = nullptr; _playerDocument = nullptr; _mediaLifetime.destroy(); _patternNow = {}; _patternNext = {}; _patternFrame = QImage(); }, [&](const GiftModel &data) { setDocument(data.document); _patternNow = {}; _patternNext = {}; _patternFrame = QImage(); }, [&](const GiftPattern &data) { const auto document = data.document; if (_document != document) { setDocument(document); const auto owner = &document->owner(); _patternNow.emoji = owner->customEmojiManager().create( document, [=] { update(); }, Data::CustomEmojiSizeTag::Large); _patternNow.map.clear(); _patternColor = QColor(); } }); } void AttributeButton::setDescriptor(const AttributeDescriptor &descriptor) { if (_descriptor == descriptor) { return; } _descriptor = descriptor; _backgroundCache = QImage(); setup(); update(); } void AttributeButton::setDocument(not_null document) { if (_document == document) { return; } _document = document; const auto media = document->createMediaView(); media->checkStickerLarge(); media->goodThumbnailWanted(); const auto destroyed = base::take(_player); _playerDocument = nullptr; _mediaLifetime = rpl::single() | rpl::then( document->session().downloaderTaskFinished() ) | rpl::filter([=] { return media->loaded(); }) | rpl::on_next([=] { _mediaLifetime.destroy(); auto result = std::unique_ptr(); const auto sticker = document->sticker(); if (sticker->isLottie()) { result = std::make_unique( ChatHelpers::LottiePlayerFromDocument( media.get(), ChatHelpers::StickerLottieSize::InlineResults, st::uniqueAttributeStickerSize, Lottie::Quality::High)); } else if (sticker->isWebm()) { result = std::make_unique( media->owner()->location(), media->bytes(), st::uniqueAttributeStickerSize); } else { result = std::make_unique( media->owner()->location(), media->bytes(), st::uniqueAttributeStickerSize); } result->setRepaintCallback([=] { update(); }); _playerDocument = media->owner(); _player = std::move(result); update(); }); if (_playerDocument) { _mediaLifetime.destroy(); } } void AttributeButton::setGeometry(QRect inner, QMargins extend) { _extend = extend; AbstractButton::setGeometry(inner.marginsAdded(extend)); } void AttributeButton::toggleSelected(bool selected, anim::type animated) { if (_selected == selected) { if (animated == anim::type::instant) { _selectedAnimation.stop(); } return; } const auto duration = st::defaultRoundCheckbox.duration; _selected = selected; if (animated == anim::type::instant) { _selectedAnimation.stop(); return; } _selectedAnimation.start([=] { update(); }, selected ? 0. : 1., selected ? 1. : 0., duration, anim::easeOutCirc); } void AttributeButton::paintBackground( QPainter &p, const QImage &background) { const auto removed = QMargins(); const auto x = removed.left(); const auto y = removed.top(); const auto width = this->width() - x - removed.right(); const auto height = this->height() - y - removed.bottom(); const auto dpr = int(background.devicePixelRatio()); const auto bwidth = background.width() / dpr; const auto bheight = background.height() / dpr; const auto fillRow = [&](int yfrom, int ytill, int bfrom) { const auto fill = [&](int xto, int wto, int xfrom, int wfrom = 0) { const auto fheight = ytill - yfrom; p.drawImage( QRect(x + xto, y + yfrom, wto, fheight), background, QRect( QPoint(xfrom, bfrom) * dpr, QSize((wfrom ? wfrom : wto), fheight) * dpr)); }; if (width < bwidth) { const auto xhalf = width / 2; fill(0, xhalf, 0); fill(xhalf, width - xhalf, bwidth - (width - xhalf)); } else if (width == bwidth) { fill(0, width, 0); } else { const auto half = bwidth / (2 * dpr); fill(0, half, 0); fill(width - half, half, bwidth - half); fill(half, width - 2 * half, half, 1); } }; if (height < bheight) { fillRow(0, height / 2, 0); fillRow(height / 2, height, bheight - (height - (height / 2))); } else { fillRow(0, height, 0); } auto hq = PainterHighQualityEnabler(p); const auto progress = _selectedAnimation.value(_selected ? 1. : 0.); if (progress < 0.01) { return; } const auto pwidth = progress * st::defaultRoundCheckbox.width; p.setPen(QPen(st::defaultRoundCheckbox.bgActive->c, pwidth)); p.setBrush(Qt::NoBrush); const auto rounded = rect().marginsRemoved(_extend); const auto phalf = pwidth / 2.; const auto extended = QRectF(rounded).marginsRemoved( { phalf, phalf, phalf, phalf }); const auto xradius = removed.left() + st::giftBoxGiftRadius - phalf; const auto yradius = removed.top() + st::giftBoxGiftRadius - phalf; p.drawRoundedRect(extended, xradius, yradius); } void AttributeButton::validatePatternCache() { if (_patternNow.emoji && _patternNow.emoji->ready()) { const auto inner = QRect(0, 0, this->width() - _extend.left() - _extend.right(), this->height() - _extend.top() - _extend.bottom()); const auto size = inner.size(); const auto ratio = style::DevicePixelRatio(); if (_patternFrame.size() != size * ratio) { _patternFrame = QImage( size * ratio, QImage::Format_ARGB32_Premultiplied); _patternFrame.setDevicePixelRatio(ratio); _patternColor = QColor(); } const auto color = _delegate->patternColor(); if (_patternColor != color) { _patternColor = color; _patternNow.map.clear(); _patternFrame.fill(Qt::transparent); auto p = QPainter(&_patternFrame); const auto skip = inner.width() / 3; Ui::PaintBgPoints( p, Ui::PatternBgPointsSmall(), _patternNow.map, _patternNow.emoji.get(), color, QRect(-skip, 0, inner.width() + 2 * skip, inner.height())); } } else if (!_patternFrame.isNull()) { _patternFrame = QImage(); } } void AttributeButton::paintEvent(QPaintEvent *e) { auto p = QPainter(this); const auto model = std::get_if(&_descriptor); const auto pattern = std::get_if(&_descriptor); const auto backdrop = std::get_if(&_descriptor); const auto position = QPoint(_extend.left(), _extend.top()); const auto background = _delegate->background(); const auto width = this->width(); const auto dpr = int(background.devicePixelRatio()); paintBackground(p, background); if (backdrop) { CacheBackdropBackground( backdrop, width, background.height() / dpr, _extend, _backgroundCache); p.drawImage(_extend.left(), _extend.top(), _backgroundCache); const auto inner = QRect( _extend.left(), _extend.top(), this->width() - _extend.left() - _extend.right(), this->height() - _extend.top() - _extend.bottom()); const auto skip = inner.width() / 3; const auto emoji = _delegate->patternEmoji(); const auto paintSymbols = [&]( PatternCache &cache, std::shared_ptr emoji, float64 shown) { if (shown == 0. || !emoji) { return; } else if (cache.emoji != emoji) { cache.map.clear(); cache.emoji = emoji; } Ui::PaintBgPoints( p, Ui::PatternBgPointsSmall(), cache.map, emoji.get(), backdrop->patternColor, QRect(-skip, 0, inner.width() + 2 * skip, inner.height()), shown); }; p.setClipRect(inner); paintSymbols(_patternNow, emoji.now, 1. - emoji.progress); paintSymbols(_patternNext, emoji.next, emoji.progress); p.setClipping(false); } else if (pattern) { p.drawImage( _extend.left(), _extend.top(), _delegate->cachePatternBackground( width, background.height() / dpr, _extend)); validatePatternCache(); if (!_patternFrame.isNull()) { p.drawImage(_extend.left(), _extend.top(), _patternFrame); } } const auto progress = _selectedAnimation.value(_selected ? 1. : 0.); if (progress > 0) { auto hq = PainterHighQualityEnabler(p); auto pen = st::boxBg->p; const auto thickness = style::ConvertScaleExact(2.); pen.setWidthF(progress * thickness); p.setPen(pen); p.setBrush(Qt::NoBrush); const auto height = background.height() / dpr; const auto outer = QRectF(0, 0, width, height); const auto shift = progress * thickness * 2; const auto extend = QMarginsF(_extend) + QMarginsF(shift, shift, shift, shift); const auto radius = st::giftBoxGiftRadius - shift; p.drawRoundedRect(outer.marginsRemoved(extend), radius, radius); } const auto paused = !isOver(); const auto now = crl::now(); const auto colored = v::is(_descriptor) ? QColor(255, 255, 255) : QColor(0, 0, 0, 0); const auto frameSize = st::uniqueAttributeStickerSize; const auto paintFrame = [&](HistoryView::StickerPlayer *player) { if (!player || !player->ready()) { return; } auto info = player->frame(frameSize, colored, false, now, paused); const auto finished = (info.index + 1 == player->framesCount()); if (!finished || !paused) { player->markFrameShown(); } const auto size = info.image.size() / style::DevicePixelRatio(); p.drawImage( QRect( (width - size.width()) / 2, st::giftBoxSmallStickerTop, size.width(), size.height()), info.image); }; if (backdrop) { const auto backdropPlayers = _delegate->backdropPlayers(); if (backdropPlayers.progress == 0.) { paintFrame(backdropPlayers.now); } else if (backdropPlayers.progress == 1.) { paintFrame(backdropPlayers.next); } else { p.setOpacity(1. - backdropPlayers.progress); paintFrame(backdropPlayers.now); p.setOpacity(backdropPlayers.progress); paintFrame(backdropPlayers.next); p.setOpacity(1.); } } else { paintFrame(_player.get()); } const auto singlew = width - _extend.left() - _extend.right(); if (model) { p.setPen(st::windowBoldFg); } else { p.setPen(QColor(255, 255, 255)); } _name.draw(p, { .position = (position + QPoint(0, st::giftBoxPremiumTextTop)), .availableWidth = singlew, .align = style::al_top, }); p.setPen(Qt::NoPen); p.setBrush(model ? anim::color(st::windowBgOver, st::windowBgActive, progress) : backdrop ? backdrop->patternColor : _delegate->patternColor()); const auto ppadding = st::uniqueAttributePercentPadding; const auto add = int(std::ceil(style::ConvertScaleExact(6.))); const auto pradius = std::max(st::giftBoxGiftRadius - add, 1); const auto left = position.x() + singlew - add - ppadding.right() - _percent.maxWidth(); const auto top = position.y() + add + ppadding.top(); const auto percent = QRect( left, top, _percent.maxWidth(), st::uniqueAttributeType.font->height); p.drawRoundedRect(percent.marginsAdded(ppadding), pradius, pradius); p.setPen(model ? anim::color(st::windowSubTextFg, st::windowFgActive, progress) : QColor(255, 255, 255)); _percent.draw(p, { .position = percent.topLeft(), }); } Delegate::Delegate(Fn fullUpdate) : _fullUpdate(std::move(fullUpdate)) { } void Delegate::update( const std::optional &now, const std::optional &next, float64 progress) { const auto wasDirty = _backdropDirty; const auto badSticker = [&]( Sticker &model, const std::optional &gift) { return gift && (model.document != gift->model.document); }; const auto enforceSticker = [&]( Sticker &model, const std::optional &gift) { Expects(gift.has_value()); const auto document = model.document = gift->model.document; const auto media = document->createMediaView(); media->checkStickerLarge(); media->goodThumbnailWanted(); const auto destroyed = base::take(model.player); model.playerDocument = nullptr; model.mediaLifetime = rpl::single() | rpl::then( document->session().downloaderTaskFinished() ) | rpl::filter([=] { return media->loaded(); }) | rpl::on_next([=, &model] { model.mediaLifetime.destroy(); auto result = std::unique_ptr(); const auto sticker = document->sticker(); if (sticker->isLottie()) { result = std::make_unique( ChatHelpers::LottiePlayerFromDocument( media.get(), ChatHelpers::StickerLottieSize::InlineResults, st::uniqueAttributeStickerSize, Lottie::Quality::High)); } else if (sticker->isWebm()) { result = std::make_unique( media->owner()->location(), media->bytes(), st::uniqueAttributeStickerSize); } else { result = std::make_unique( media->owner()->location(), media->bytes(), st::uniqueAttributeStickerSize); } result->setRepaintCallback(_fullUpdate); model.playerDocument = media->owner(); model.player = std::move(result); _fullUpdate(); }); if (model.playerDocument) { model.mediaLifetime.destroy(); } }; const auto validateSticker = [&]( Sticker &model, const std::optional &gift) { if (badSticker(model, gift)) { enforceSticker(model, gift); } }; const auto validatePatternEmoji = [&]( Emoji &emoji, const std::optional &gift) { if (!gift) { emoji = {}; return; } const auto document = gift->pattern.document; if (emoji.document != document) { emoji.document = document; emoji.custom = document->owner().customEmojiManager().create( document, _fullUpdate, Data::CustomEmojiSizeTag::Large); } }; if (progress == 1.) { Assert(next.has_value()); if (_nextBackdrop != next->backdrop) { _nextBackdrop = next->backdrop; _backdropDirty = true; } validateSticker(_nextModel, next); validatePatternEmoji(_nextPatternEmoji, next); _nowModel = {}; _nowPatternEmoji = {}; } else { Assert(now.has_value()); if (_nowBackdrop != now->backdrop) { if (_nextBackdrop == now->backdrop) { _nowBackdrop = base::take(_nextBackdrop); } else { _nowBackdrop = now->backdrop; _backdropDirty = true; } } if (badSticker(_nowModel, now)) { if (_nextModel.document == now->model.document && !_nextModel.mediaLifetime) { _nowModel = base::take(_nextModel); } else { enforceSticker(_nowModel, now); } } validatePatternEmoji(_nowPatternEmoji, now); if (progress > 0.) { Assert(next.has_value()); if (_nextBackdrop != next->backdrop) { _nextBackdrop = next->backdrop; _backdropDirty = true; } validateSticker(_nextModel, next); validatePatternEmoji(_nextPatternEmoji, next); } else { if (_nextModel.document) { _nextModel = Sticker(); } if (_nextPatternEmoji.document) { _nextPatternEmoji = Emoji(); } } } if (_progress != progress) { _progress = progress; _backdropDirty = true; } if (!wasDirty && _backdropDirty) { _fullUpdate(); } } QSize Delegate::buttonSize() { if (!_single.isEmpty()) { return _single; } const auto width = st::boxWideWidth; const auto padding = st::giftBoxPadding; const auto available = width - padding.left() - padding.right(); const auto singlew = (available - 2 * st::giftBoxGiftSkip.x()) / kGiftsPerRow; _single = QSize(singlew, st::giftBoxGiftSmall); return _single; } QMargins Delegate::buttonExtend() const { return st::defaultDropdownMenu.wrap.shadow.extend; } QImage Delegate::background() { if (!_bg.isNull()) { return _bg; } const auto single = buttonSize(); const auto extend = buttonExtend(); const auto bgSize = single.grownBy(extend); const auto ratio = style::DevicePixelRatio(); auto bg = QImage( bgSize * ratio, QImage::Format_ARGB32_Premultiplied); bg.setDevicePixelRatio(ratio); bg.fill(Qt::transparent); const auto radius = st::giftBoxGiftRadius; const auto rect = QRect(QPoint(), bgSize).marginsRemoved(extend); { auto p = QPainter(&bg); auto hq = PainterHighQualityEnabler(p); p.setOpacity(0.3); p.setPen(Qt::NoPen); p.setBrush(st::windowShadowFg); p.drawRoundedRect( QRectF(rect).translated(0, radius / 12.), radius, radius); } bg = bg.scaled( (bgSize * ratio) / 2, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); bg = Images::Blur(std::move(bg), true); bg = bg.scaled( bgSize * ratio, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); { auto p = QPainter(&bg); auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); p.setBrush(st::windowBg); p.drawRoundedRect(rect, radius, radius); } _bg = std::move(bg); return _bg; } QImage Delegate::cachePatternBackground( int width, int height, QMargins extend) { if (_backdropDirty) { _backdrop = GiftBackdrop(); if (_progress == 0.) { _backdrop = _nowBackdrop; } else if (_progress == 1.) { _backdrop = _nextBackdrop; } else { _backdrop.centerColor = anim::color( _nowBackdrop.centerColor, _nextBackdrop.centerColor, _progress); _backdrop.edgeColor = anim::color( _nowBackdrop.edgeColor, _nextBackdrop.edgeColor, _progress); _backdrop.patternColor = anim::color( _nowBackdrop.patternColor, _nextBackdrop.patternColor, _progress); } } CacheBackdropBackground( &_backdrop, width, height, extend, _backdropCache, _backdropDirty); _backdropDirty = false; return _backdropCache; } QColor Delegate::patternColor() { Expects(!_backdropDirty); return _backdrop.patternColor; } BackdropPlayers Delegate::backdropPlayers() { return { .now = _nowModel.player.get(), .next = _nextModel.player.get(), .progress = _progress, }; } PatternEmoji Delegate::patternEmoji() { return { .now = _nowPatternEmoji.custom, .next = _nextPatternEmoji.custom, .progress = _progress, }; } AttributesList::AttributesList( QWidget *parent, not_null delegate, not_null attributes, rpl::producer tab, Selection initialSelection) : BoxContentDivider(parent) , _delegate(delegate) , _attributes(attributes) , _tab(std::move(tab)) , _selected(initialSelection) , _entries(&_models) , _list(&_entries->list) { _singleMin = _delegate->buttonSize(); fill(); _tab.value( ) | rpl::on_next([=](Tab tab) { _entries = [&] { switch (tab) { case Tab::Model: return &_models; case Tab::Pattern: return &_patterns; case Tab::Backdrop: return &_backdrops; } Unexpected("Tab in AttributesList."); }(); _list = &_entries->list; refreshButtons(); refreshAbout(); }, lifetime()); _selected.value() | rpl::combine_previous() | rpl::on_next([=]( Selection was, Selection now) { const auto tab = _tab.current(); const auto button = [&](int index) { const auto i = ranges::find(_views, index, &View::index); return (i != end(_views)) ? i->button.get() : nullptr; }; const auto update = [&](int was, int now) { if (was != now) { if (const auto deselect = button(was)) { deselect->toggleSelected(false, anim::type::normal); } if (const auto select = button(now)) { select->toggleSelected(true, anim::type::normal); } } }; if (tab == Tab::Model) { update(was.model, now.model); } else if (tab == Tab::Pattern) { update(was.pattern, now.pattern); } else { update(was.backdrop, now.backdrop); } }, lifetime()); } auto AttributesList::selected() const -> rpl::producer { return _selected.value(); } void AttributesList::fill() { const auto addEntries = [&]( const auto &source, Entries &target, const auto field) { auto value = Selection(); target.list.clear(); target.list.reserve(source.size()); for (const auto &item : source) { ++(value.*field); target.list.emplace_back(Entry{ { item }, { value } }); } }; addEntries(_attributes->models, _models, &Selection::model); addEntries(_attributes->patterns, _patterns, &Selection::pattern); addEntries(_attributes->backdrops, _backdrops, &Selection::backdrop); } void AttributesList::visibleTopBottomUpdated( int visibleTop, int visibleBottom) { _visibleFrom = visibleTop; _visibleTill = visibleBottom; validateButtons(); } void AttributesList::paintEvent(QPaintEvent *e) { auto p = QPainter(this); p.fillRect(rect(), st::boxDividerBg->c); paintTop(p); paintBottom(p); } void AttributesList::refreshButtons() { _viewsForWidth = 0; _viewsFromRow = 0; _viewsTillRow = 0; resizeToWidth(width()); validateButtons(); } void AttributesList::validateButtons() { if (!_perRow || !_about) { return; } const auto padding = st::giftBoxPadding; const auto vskip = st::giftListAboutMargin.top() + _about->height() + st::giftListAboutMargin.bottom(); const auto row = _single.height() + st::giftBoxGiftSkip.y(); const auto fromRow = std::max(_visibleFrom - vskip, 0) / row; const auto tillRow = (_visibleTill - vskip + row - 1) / row; Assert(tillRow >= fromRow); if (_viewsFromRow == fromRow && _viewsTillRow == tillRow && _viewsForWidth == width()) { return; } _viewsFromRow = fromRow; _viewsTillRow = tillRow; _viewsForWidth = width(); const auto available = _viewsForWidth - padding.left() - padding.right(); const auto skipw = st::giftBoxGiftSkip.x(); const auto fullw = _perRow * (_single.width() + skipw) - skipw; const auto left = padding.left() + (available - fullw) / 2; const auto oneh = _single.height() + st::giftBoxGiftSkip.y(); auto x = left; auto y = vskip + fromRow * oneh; auto views = std::vector(); views.reserve((tillRow - fromRow) * _perRow); const auto idUsed = [&](DocumentData *document, int column, int row) { if (!document) { return false; } for (auto j = row; j != tillRow; ++j) { for (auto i = column; i != _perRow; ++i) { const auto index = j * _perRow + i; if (index >= _list->size()) { return false; } else if (Sticker((*_list)[index].descriptor) == document) { return true; } } column = 0; } return false; }; const auto selected = [&] { const auto values = _selected.current(); switch (_tab.current()) { case Tab::Model: return values.model; case Tab::Pattern: return values.pattern; case Tab::Backdrop: return values.backdrop; } Unexpected("Tab in AttributesList::validateButtons."); }(); const auto add = [&](int column, int row) { const auto index = row * _perRow + column; if (index >= _list->size()) { return false; } const auto &entry = (*_list)[index]; const auto &descriptor = entry.descriptor; const auto document = Sticker(descriptor); const auto already = ranges::find_if(_views, [&](const View &view) { return view.button && view.document == document; }); if (already != end(_views)) { views.push_back(base::take(*already)); views.back().button->setDescriptor(descriptor); } else { const auto unused = ranges::find_if(_views, [&](const View &v) { return v.button && !idUsed(v.document, column, row); }); if (unused != end(_views)) { views.push_back(base::take(*unused)); views.back().document = document; views.back().button->setDescriptor(descriptor); } else { views.push_back({ .button = std::make_unique( this, _delegate, descriptor), .document = document, }); views.back().button->show(); } } auto &view = views.back(); view.index = index; view.button->toggleSelected(index == selected, anim::type::instant); view.button->setClickedCallback([=] { clicked(index); }); return true; }; for (auto j = fromRow; j != tillRow; ++j) { for (auto i = 0; i != _perRow; ++i) { if (!add(i, j)) { break; } const auto &view = views.back(); auto pos = QPoint(x, y); view.button->setGeometry( QRect(pos, _single), _delegate->buttonExtend()); x += _single.width() + skipw; } x = left; y += oneh; } std::swap(_views, views); } void AttributesList::clicked(int index) { Expects(index >= 0 && index < _list->size()); auto now = _selected.current(); const auto value = (*_list)[index].value; const auto check = [&](const auto field) { if (value.*field >= 0) { now.*field = (now.*field == value.*field) ? -1 : value.*field; } }; check(&Selection::model); check(&Selection::pattern); check(&Selection::backdrop); _selected = now; } void AttributesList::refreshAbout() { auto text = [&] { switch (_tab.current()) { case Tab::Model: return tr::lng_auction_preview_models; case Tab::Pattern: return tr::lng_auction_preview_symbols; case Tab::Backdrop: return tr::lng_auction_preview_backdrops; } Unexpected("Tab in AttributesList::refreshAbout."); }()(lt_count_decimal, rpl::single(_list->size() * 1.), tr::rich); auto about = std::make_unique( this, std::move(text), st::giftListAbout); about->show(); _about = std::move(about); resizeToWidth(width()); } int AttributesList::resizeGetHeight(int width) { const auto padding = st::giftBoxPadding; const auto count = int(_list->size()); const auto available = width - padding.left() - padding.right(); const auto skipw = st::giftBoxGiftSkip.x(); _perRow = std::min( (available + skipw) / (_singleMin.width() + skipw), std::max(count, 1)); if (!_perRow || !_about) { return 0; } auto result = 0; const auto margin = st::giftListAboutMargin; _about->resizeToWidth(width - margin.left() - margin.right()); _about->moveToLeft(margin.left(), result + margin.top()); result += margin.top() + _about->height() + margin.bottom(); const auto singlew = std::min( ((available + skipw) / _perRow) - skipw, 2 * _singleMin.width()); Assert(singlew >= _singleMin.width()); const auto singleh = _singleMin.height(); _single = QSize(singlew, singleh); const auto rows = (count + _perRow - 1) / _perRow; const auto skiph = st::giftBoxGiftSkip.y(); result += rows ? (padding.bottom() + rows * (singleh + skiph) - skiph) : 0; return result; } } // namespace void StarGiftPreviewBox( not_null box, const QString &title, const Data::UniqueGiftAttributes &attributes, Data::GiftAttributeIdType tab, std::shared_ptr selected) { Expects(!attributes.models.empty()); Expects(!attributes.patterns.empty()); Expects(!attributes.backdrops.empty()); box->setStyle(st::uniqueAttributesBox); box->setWidth(st::boxWideWidth); box->setNoContentMargin(true); struct State { State( QString title, const Data::UniqueGiftAttributes &attributes, Data::GiftAttributeIdType tab, std::shared_ptr selected) : title(title) , delegate([=] { if (this->tab.current() != Tab::Model && list) { list->update(); } }) , attributes(attributes) , tab(tab) , gift(make()) , pushNextTimer([=] { push(); }) { apply(selected); } void apply(std::shared_ptr selected) { if (!selected) { return; } const auto up = [&](auto &list, const auto &attribute) { ranges::stable_partition(list, [&](const auto &existing) { return IdFor(attribute) == IdFor(existing); }); }; up(attributes.models, selected->model); up(attributes.patterns, selected->pattern); up(attributes.backdrops, selected->backdrop); fixed = { 0, 0, 0 }; paused = true; gift = make(); } void randomize() { const auto choose = [](const auto &list, auto &indices) { if (indices.empty()) { ranges::copy( ranges::views::ints(0, int(list.size())), std::back_inserter(indices)); } const auto which = base::RandomIndex(indices.size()); const auto index = indices[which]; indices.erase(begin(indices) + which); return index; }; index = { .model = choose(attributes.models, models), .pattern = choose(attributes.patterns, patterns), .backdrop = choose(attributes.backdrops, backdrops), }; } [[nodiscard]] UniqueGiftCover make() { randomize(); const auto choose = [](const auto &list, int index, int fixed) { return list[(fixed >= 0) ? fixed : index]; }; return { .values = { .title = title, .model = choose( attributes.models, index.model, fixed.model), .pattern = choose( attributes.patterns, index.pattern, fixed.pattern), .backdrop = choose( attributes.backdrops, index.backdrop, fixed.backdrop), }, .force = paused.current(), }; } void push() { gift = make(); } QString title; Delegate delegate; Data::UniqueGiftAttributes attributes; std::vector models; std::vector patterns; std::vector backdrops; rpl::variable tab = Tab::Model; Selection index; Selection fixed; rpl::variable paused; rpl::variable gift; AttributesList *list = nullptr; base::Timer pushNextTimer; }; const auto state = box->lifetime().make_state( title, attributes, tab, selected); const auto repaintedHook = [=]( std::optional now, std::optional next, float64 progress) { state->delegate.update(now, next, progress); }; const auto top = box->setPinnedToTopContent( object_ptr(box)); AddUniqueGiftCover(top, state->gift.value(), { .subtitle = rpl::conditional( state->paused.value(), tr::lng_auction_preview_selected(tr::marked), tr::lng_auction_preview_random(tr::marked)), .attributesInfo = true, .repaintedHook = repaintedHook, }); AddUniqueCloseButton(box, {}); const auto container = box->verticalLayout(); state->list = container->add(object_ptr( box, &state->delegate, &state->attributes, state->tab.value(), state->fixed)); state->list->selected( ) | rpl::on_next([=](Selection value) { state->fixed = value; state->paused = (value.model >= 0) || (value.pattern >= 0) || (value.backdrop >= 0); state->push(); }, box->lifetime()); const auto button = box->addButton(rpl::single(u""_q), [] {}); const auto buttonsParent = button->parentWidget(); box->clearButtons(); const auto add = [&]( tr::phrase<> text, const style::RoundButton &st, const style::icon &active, Tab tab) { auto owned = object_ptr(buttonsParent, text(), st); const auto raw = owned.data(); raw->setTextTransform(RoundButton::TextTransform::NoTransform); raw->setClickedCallback([=] { state->tab = tab; }); const auto icon = &active; state->tab.value() | rpl::on_next([=](Tab now) { raw->setTextFgOverride((now == tab) ? st::defaultActiveButton.textFg->c : std::optional()); raw->setBrushOverride((now == tab) ? st::defaultActiveButton.textBg->c : std::optional()); raw->setIconOverride((now == tab) ? icon : nullptr); }, raw->lifetime()); box->addButton(std::move(owned)); }; add( tr::lng_auction_preview_symbols_button, st::uniqueAttributeSymbol, st::uniqueAttributeSymbolActive, Tab::Pattern); add( tr::lng_auction_preview_backdrops_button, st::uniqueAttributeBackdrop, st::uniqueAttributeBackdropActive, Tab::Backdrop); add( tr::lng_auction_preview_models_button, st::uniqueAttributeModel, st::uniqueAttributeModelActive, Tab::Model); state->paused.value() | rpl::on_next([=](bool paused) { if (paused) { state->pushNextTimer.cancel(); } else { state->pushNextTimer.callEach(kSwitchTimeout); } }, box->lifetime()); } } // namespace Ui