/* 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 "ui/boxes/emoji_stake_box.h" #include "base/event_filter.h" #include "base/object_ptr.h" #include "chat_helpers/stickers_lottie.h" #include "data/components/credits.h" #include "data/stickers/data_custom_emoji.h" #include "data/data_document.h" #include "data/data_session.h" #include "info/channel_statistics/earn/earn_format.h" #include "info/channel_statistics/earn/earn_icons.h" #include "lang/lang_keys.h" #include "lottie/lottie_icon.h" #include "main/main_app_config.h" #include "main/main_session.h" #include "settings/settings_common.h" // CreateLottieIcon #include "settings/settings_credits_graphics.h" // AddBalanceWidget #include "ui/controls/ton_common.h" #include "ui/text/custom_emoji_helper.h" #include "ui/text/custom_emoji_text_badge.h" #include "ui/text/text_lottie_custom_emoji.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/fields/number_input.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/painter.h" #include "ui/vertical_list.h" #include "styles/style_calls.h" // confcallJoinBox #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_credits.h" // creditsHistoryRightSkip #include "styles/style_layers.h" #include "styles/style_settings.h" // settingsCloudPasswordIconSize #include "styles/style_widgets.h" #include namespace Ui { namespace { [[nodiscard]] not_null AddMoneyInputIcon( not_null parent, Text::PaletteDependentEmoji emoji) { auto helper = Text::CustomEmojiHelper(); auto text = helper.paletteDependent(std::move(emoji)); return CreateChild( parent, rpl::single(std::move(text)), st::defaultFlatLabel, st::defaultPopupMenu, helper.context()); } [[nodiscard]] QImage MakeEmojiFrame(int index, int size) { const auto path = u":/gui/dice/dice%1.svg"_q.arg(index); const auto ratio = style::DevicePixelRatio(); auto svg = QSvgRenderer(path); auto result = QImage( QSize(size, size) * ratio, QImage::Format_ARGB32_Premultiplied); result.setDevicePixelRatio(ratio); result.fill(Qt::transparent); auto p = QPainter(&result); svg.render(&p, QRect(0, 0, size, size)); p.end(); return result; } [[nodiscard]] object_ptr MakeLogo(not_null box) { const auto &size = st::settingsCloudPasswordIconSize; auto icon = Settings::CreateLottieIcon( box->verticalLayout(), { .name = u"dice_6"_q, .sizeOverride = { size, size } }, QMargins()); const auto animate = std::move(icon.animate); box->showFinishes() | rpl::take(1) | rpl::on_next([=] { animate(anim::repeat::once); }, box->lifetime()); return std::move(icon.widget); } [[nodiscard]] object_ptr MakeTable( not_null parent, const EmojiGameStakeArgs &args) { auto result = object_ptr(parent); const auto raw = result.data(); struct State { std::array dices; std::array multiplicators; }; const auto state = raw->lifetime().make_state(); const auto serialize = [&](int milliReward) { return QString(QChar(0xD7)) + QString::number(milliReward / 1000.); }; const auto esize = st::stakeEmojiSize; for (auto i = 0; i != 6; ++i) { state->dices[i] = MakeEmojiFrame(i + 1, esize); const auto value = (i < args.milliRewards.size()) ? args.milliRewards[i] : 0; state->multiplicators[i].setText( st::semiboldTextStyle, serialize(value)); } state->multiplicators[6].setText( st::semiboldTextStyle, serialize(args.jackpotMilliReward)); raw->paintOn([=](QPainter &p) { auto path = QPainterPath(); const auto &st = st::defaultTable; const auto border = st.border; const auto half = border / 2.; const auto add = QMargins(border, border, border, border); const auto inner = raw->rect().marginsRemoved(add); path.addRoundedRect(inner, st.radius, st.radius); { const auto y = border + inner.height() / 2.; path.moveTo(border + half, y); path.lineTo(border + inner.width() - half, y); } { const auto x = border + inner.width() / 2.; path.moveTo(x, border + half); path.lineTo(x, border + inner.height() - half); } { const auto x = border + (inner.width() - border) / 4.; path.moveTo(x, border + half); path.lineTo(x, border + inner.height() - half); } { const auto x = border + (inner.width() / 2.) + (inner.width() - border) / 4.; path.moveTo(x, border + half); path.lineTo(x, border + (inner.height() / 2.) - half); } auto pen = st.borderFg->p; pen.setWidth(st.border); p.setPen(pen); p.setBrush(Qt::NoBrush); auto hq = PainterHighQualityEnabler(p); p.drawPath(path); const auto etop = st::stakeEmojiTop; const auto ttop = etop + esize; { auto left = border + half; const auto width = (inner.width() - 3 * border) / 4.; auto top = border + half; for (auto i = 0; i != 7; ++i, left += width + border) { if (i == 4) { left -= 4 * (width + border); top += border + half + inner.height() / 2.; } const auto x = int(base::SafeRound(left)); const auto y = int(base::SafeRound(top)); const auto right = (i < 6) ? int(base::SafeRound(left + width)) : int(base::SafeRound(left + 2 * width + border)); const auto w = (right - x); if (i < 6) { p.drawImage( x + (w - esize) / 2, y + etop, state->dices[i]); } else { const auto &last = state->dices.back(); for (auto j = 0; j != 3; ++j) { p.drawImage( x + (w - 3 * esize) / 2 + (esize * j), y + etop, last); } } p.setPen(st::windowBoldFg); const auto &mul = state->multiplicators[i]; mul.draw(p, { .position = { x + (w - mul.maxWidth()) / 2, y + ttop }, }); } } }); raw->widthValue() | rpl::on_next([=](int width) { raw->resize(width, width / 3); }, raw->lifetime()); return result; } } // namespace not_null AddStarsInputField( not_null container, StarsInputFieldArgs &&args) { const auto wrap = container->add( object_ptr( container, st::editTagField.heightMin), st::boxRowPadding); const auto result = CreateChild( wrap, st::editTagField, rpl::single(u"0"_q), args.value ? QString::number(*args.value) : QString(), args.max ? args.max : std::numeric_limits::max()); const auto icon = AddMoneyInputIcon( result, Earn::IconCreditsEmoji()); wrap->widthValue() | rpl::on_next([=](int width) { icon->move(st::starsFieldIconPosition); result->move(0, 0); result->resize(width, result->height()); wrap->resize(width, result->height()); }, wrap->lifetime()); return result; } not_null AddTonInputField( not_null container, TonInputFieldArgs &&args) { const auto wrap = container->add( object_ptr( container, st::editTagField.heightMin), st::boxRowPadding); const auto result = CreateTonAmountInput( wrap, rpl::single('0' + TonAmountSeparator() + '0'), args.value); const auto icon = AddMoneyInputIcon( result, Earn::IconCurrencyEmoji()); wrap->widthValue() | rpl::on_next([=](int width) { icon->move(st::tonFieldIconPosition); result->move(0, 0); result->resize(width, result->height()); wrap->resize(width, result->height()); }, wrap->lifetime()); return result; } void AddApproximateUsd( not_null field, not_null session, rpl::producer price) { auto value = std::move(price) | rpl::map([=](CreditsAmount amount) { if (!amount) { return QString(); } const auto appConfig = &session->appConfig(); const auto rate = amount.ton() ? appConfig->currencySellRate() : (appConfig->starsSellRate() / 100.); return Info::ChannelEarn::ToUsd(amount, rate, 2); }); const auto usd = Ui::CreateChild( field, std::move(value), st::suggestPriceEstimate); const auto move = [=] { usd->moveToRight(0, st::suggestPriceEstimateTop); }; base::install_event_filter(field, [=](not_null e) { if (e->type() == QEvent::Resize) { move(); } return base::EventFilterResult::Continue; }); usd->widthValue() | rpl::on_next(move, usd->lifetime()); } void EmojiGameStakeBox( not_null box, EmojiGameStakeArgs &&args) { box->setStyle(st::confcallJoinBox); box->setWidth(st::boxWideWidth); box->setNoContentMargin(true); const auto container = box->verticalLayout(); const auto skip = st::confcallLinkHeaderIconPadding.bottom(); box->addRow( MakeLogo(box), st::boxRowPadding + QMargins(0, 0, 0, skip), style::al_top); auto helper = Text::CustomEmojiHelper(); const auto beta = helper.paletteDependent( Text::CustomEmojiTextBadge( tr::lng_stake_game_beta(tr::now), st::customEmojiTextBadge)); const auto sixText = helper.image({ .image = MakeEmojiFrame(6, st::emojiSize), .textColor = false, }); auto title = tr::lng_stake_game_title( tr::marked ) | rpl::map([=](TextWithEntities &&text) { return text.append(' ').append(beta); }); box->addRow( object_ptr( box, std::move(title), st::boxTitle, st::defaultPopupMenu, helper.context()), st::boxRowPadding + st::confcallLinkTitlePadding, style::al_top); box->addRow( object_ptr( box, tr::lng_stake_game_about(tr::rich), st::confcallLinkCenteredText), st::boxRowPadding + QMargins(0, 0, 0, skip), style::al_top )->setTryMakeSimilarLines(true); const auto sub = st::boxRowPadding - st::defaultSubsectionTitlePadding; const auto subtitlePadding = QMargins(sub.left(), 0, sub.right(), 0); AddSubsectionTitle( container, tr::lng_stake_game_results(), subtitlePadding); const auto half = (st::defaultTable.border + 1) / 2; box->addRow( MakeTable(box, args), st::boxRowPadding + QMargins(-half, 0, -half, skip / 2), style::al_top); const auto sixContext = helper.context(); const auto factory = [=]( QStringView data, const Text::MarkedContext &context) { if (auto result = sixContext.customEmojiFactory(data, context)) { return std::make_unique( std::move(result), 0, true); } return std::unique_ptr(); }; const auto context = Text::MarkedContext{ .customEmojiFactory = factory, }; const auto resets = box->addRow( object_ptr( box, tr::lng_stake_game_resets( lt_emoji, rpl::single(sixText), tr::rich), st::confcallLinkFooterOr, st::defaultPopupMenu, context), st::boxRowPadding, style::al_top); resets->setTryMakeSimilarLines(true); resets->setAnimationsPausedCallback([=] { return FlatLabel::WhichAnimationsPaused::All; }); AddSubsectionTitle( container, tr::lng_stake_game_your(), subtitlePadding + QMargins(0, skip, 0, sub.bottom())); const auto field = AddTonInputField(container, { .value = args.currentStake, }); box->setFocusCallback([=] { field->setFocusFast(); }); const auto price = field->lifetime().make_state>( args.currentStake); auto priceValue = price->value() | rpl::map([=](int64 amount) { return CreditsAmount( amount / Ui::kNanosInOne, amount % Ui::kNanosInOne, CreditsType::Ton); }); AddApproximateUsd( field, args.session, std::move(priceValue)); field->changes() | rpl::on_next([=] { *price = Ui::ParseTonAmountString(field->getLastText()).value_or(0); }, field->lifetime()); const auto submit = args.submit; const auto callback = [=] { const auto text = field->getLastText(); const auto now = Ui::ParseTonAmountString(text).value_or(0); const auto appConfig = &args.session->appConfig(); const auto min = appConfig->stakeDiceNanoTonMin(); const auto max = appConfig->stakeDiceNanoTonMax(); if (!now || now < min || now > max) { field->showError(); } else { box->closeBox(); submit(now); } }; field->submits() | rpl::on_next(callback, field->lifetime()); const auto button = box->addButton(rpl::single(QString()), callback); button->setText(tr::lng_stake_game_save_and_roll( ) | rpl::map([=](QString text) { return Text::IconEmoji( &st::stakeEmojiIcon, QString::fromUtf8("\xf0\x9f\x8e\xb2") ).append(' ').append(text); })); const auto close = CreateChild( container, st::boxTitleClose); close->setClickedCallback([=] { box->closeBox(); }); container->widthValue() | rpl::on_next([=](int) { close->moveToRight(0, 0); }, close->lifetime()); const auto session = args.session; session->credits().tonLoad(true); const auto balance = Settings::AddBalanceWidget( container, session, session->credits().tonBalanceValue(), false); rpl::combine( balance->sizeValue(), container->sizeValue() ) | rpl::on_next([=](const QSize &, const QSize &) { balance->moveToLeft( st::creditsHistoryRightSkip * 2, st::creditsHistoryRightSkip); balance->update(); }, balance->lifetime()); } } // namespace Ui