Files
tdesktop/Telegram/SourceFiles/boxes/star_gift_preview_box.cpp
2025-12-10 21:28:33 +03:00

1421 lines
38 KiB
C++

/*
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 "window/window_session_controller.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<GiftModel, GiftPattern, GiftBackdrop> {
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<UniqueGiftPattern>(&value)) {
return pattern->document;
} else if (const auto model = std::get_if<UniqueGiftModel>(&value)) {
return model->document;
}
return nullptr;
}
struct BackdropPlayers {
HistoryView::StickerPlayer *now = nullptr;
HistoryView::StickerPlayer *next = nullptr;
float64 progress = 0.;
};
struct PatternEmoji {
std::shared_ptr<Text::CustomEmoji> now;
std::shared_ptr<Text::CustomEmoji> 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<AttributeDelegate*> 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<float64, QImage> map;
std::shared_ptr<Text::CustomEmoji> emoji;
};
void paintEvent(QPaintEvent *e) override;
void paintBackground(QPainter &p, const QImage &background);
void setup();
void validatePatternCache();
void setDocument(not_null<DocumentData*> document);
const not_null<AttributeDelegate*> _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<HistoryView::StickerPlayer> _player;
rpl::lifetime _mediaLifetime;
};
class Delegate final : public AttributeDelegate {
public:
explicit Delegate(Fn<void()> fullUpdate);
Delegate(Delegate &&other) = default;
~Delegate() = default;
void update(
const std::optional<Data::UniqueGift> &now,
const std::optional<Data::UniqueGift> &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<HistoryView::StickerPlayer> player;
rpl::lifetime mediaLifetime;
};
struct Emoji {
DocumentData *document;
std::shared_ptr<Text::CustomEmoji> custom;
};
Fn<void()> _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*> delegate,
not_null<Window::SessionController*> window,
not_null<const Data::UniqueGiftAttributes*> attributes,
rpl::producer<Tab> tab);
[[nodiscard]] rpl::producer<Selection> selected() const;
private:
struct Entry {
AttributeDescriptor descriptor;
Selection value;
};
struct Entries {
std::vector<Entry> list;
};
struct View {
std::unique_ptr<AttributeButton> 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*> _delegate;
const not_null<Window::SessionController*> _window;
const not_null<const Data::UniqueGiftAttributes*> _attributes;
rpl::variable<Tab> _tab;
rpl::variable<Selection> _selected;
std::unique_ptr<Ui::RpWidget> _about;
Entries _models;
Entries _patterns;
Entries _backdrops;
not_null<Entries*> _entries;
not_null<std::vector<Entry>*> _list;
std::vector<View> _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<GiftBackdrop*> 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<AttributeDelegate*> 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<DocumentData*> 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<HistoryView::StickerPlayer>();
const auto sticker = document->sticker();
if (sticker->isLottie()) {
result = std::make_unique<HistoryView::LottiePlayer>(
ChatHelpers::LottiePlayerFromDocument(
media.get(),
ChatHelpers::StickerLottieSize::InlineResults,
st::uniqueAttributeStickerSize,
Lottie::Quality::High));
} else if (sticker->isWebm()) {
result = std::make_unique<HistoryView::WebmPlayer>(
media->owner()->location(),
media->bytes(),
st::uniqueAttributeStickerSize);
} else {
result = std::make_unique<HistoryView::StaticStickerPlayer>(
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<GiftModel>(&_descriptor);
const auto pattern = std::get_if<GiftPattern>(&_descriptor);
const auto backdrop = std::get_if<GiftBackdrop>(&_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<Text::CustomEmoji> 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<GiftPattern>(_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<void()> fullUpdate)
: _fullUpdate(std::move(fullUpdate)) {
}
void Delegate::update(
const std::optional<Data::UniqueGift> &now,
const std::optional<Data::UniqueGift> &next,
float64 progress) {
const auto wasDirty = _backdropDirty;
const auto badSticker = [&](
Sticker &model,
const std::optional<Data::UniqueGift> &gift) {
return gift && (model.document != gift->model.document);
};
const auto enforceSticker = [&](
Sticker &model,
const std::optional<Data::UniqueGift> &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<HistoryView::StickerPlayer>();
const auto sticker = document->sticker();
if (sticker->isLottie()) {
result = std::make_unique<HistoryView::LottiePlayer>(
ChatHelpers::LottiePlayerFromDocument(
media.get(),
ChatHelpers::StickerLottieSize::InlineResults,
st::uniqueAttributeStickerSize,
Lottie::Quality::High));
} else if (sticker->isWebm()) {
result = std::make_unique<HistoryView::WebmPlayer>(
media->owner()->location(),
media->bytes(),
st::uniqueAttributeStickerSize);
} else {
result = std::make_unique<HistoryView::StaticStickerPlayer>(
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<Data::UniqueGift> &gift) {
if (badSticker(model, gift)) {
enforceSticker(model, gift);
}
};
const auto validatePatternEmoji = [&](
Emoji &emoji,
const std::optional<Data::UniqueGift> &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*> delegate,
not_null<Window::SessionController*> window,
not_null<const Data::UniqueGiftAttributes*> attributes,
rpl::producer<Tab> tab)
: BoxContentDivider(parent)
, _delegate(delegate)
, _window(window)
, _attributes(attributes)
, _tab(std::move(tab))
, _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<Selection> {
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<View>();
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().button->setDescriptor(descriptor);
} else {
views.push_back({
.button = std::make_unique<AttributeButton>(
this,
_delegate,
descriptor)
});
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<Ui::FlatLabel>(
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<GenericBox*> box,
not_null<Window::SessionController*> controller,
const Data::StarGift &gift,
const Data::UniqueGiftAttributes &attributes) {
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)
: title(title)
, delegate([=] {
if (tab.current() != Tab::Model && list) {
list->update();
}
})
, attributes(attributes)
, gift(make())
, pushNextTimer([=] { push(); }) {
}
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<int> models;
std::vector<int> patterns;
std::vector<int> backdrops;
rpl::variable<Tab> tab = Tab::Model;
Selection index;
Selection fixed;
rpl::variable<bool> paused;
rpl::variable<UniqueGiftCover> gift;
AttributesList *list = nullptr;
base::Timer pushNextTimer;
};
const auto title = gift.resellTitle;
const auto state = box->lifetime().make_state<State>(title, attributes);
const auto repaintedHook = [=](
std::optional<Data::UniqueGift> now,
std::optional<Data::UniqueGift> next,
float64 progress) {
state->delegate.update(now, next, progress);
};
const auto container = box->verticalLayout();
AddUniqueGiftCover(container, 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, {});
state->list = container->add(object_ptr<AttributesList>(
box,
&state->delegate,
controller,
&state->attributes,
state->tab.value()));
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<RoundButton>(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<QColor>());
raw->setBrushOverride((now == tab)
? st::defaultActiveButton.textBg->c
: std::optional<QColor>());
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