Added list of options to box for giveaway creation with credits.

This commit is contained in:
23rd 2024-08-28 00:36:25 +03:00 committed by John Preston
parent e6b8b4be18
commit 51030e3c45
2 changed files with 371 additions and 44 deletions

View file

@ -2812,6 +2812,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_giveaway_awarded_body_group" = "Check your groups' {link} to see how gifts boosted your group."; "lng_giveaway_awarded_body_group" = "Check your groups' {link} to see how gifts boosted your group.";
"lng_giveaway_created_link" = "Statistics"; "lng_giveaway_created_link" = "Statistics";
"lng_giveaway_credits_options_title" = "Stars to distribute";
"lng_giveaway_credits_option_status#one" = "{count} per user";
"lng_giveaway_credits_option_status#other" = "{count} per user";
"lng_giveaway_credits_options_about" = "Choose how many stars to give away and how many boosts to receive for 1 year.";
"lng_giveaway_credits_quantity_title" = "Number of winners";
"lng_giveaway_credits_quantity_about" = "Choose how many winners you want to distribute stars among.";
"lng_prize_title" = "Congratulations!"; "lng_prize_title" = "Congratulations!";
"lng_prize_about" = "You won a prize in a giveaway organized by {channel}."; "lng_prize_about" = "You won a prize in a giveaway organized by {channel}.";
"lng_prize_duration" = "Your prize is a **Telegram Premium** subscription {duration}."; "lng_prize_duration" = "Your prize is a **Telegram Premium** subscription {duration}.";

View file

@ -20,33 +20,40 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "info/channel_statistics/boosts/info_boosts_widget.h" #include "info/channel_statistics/boosts/info_boosts_widget.h"
#include "info/info_controller.h" #include "info/info_controller.h"
#include "info/info_memento.h" #include "info/info_memento.h"
#include "info/statistics/info_statistics_list_controllers.h"
#include "lang/lang_keys.h" #include "lang/lang_keys.h"
#include "main/main_session.h"
#include "payments/payments_checkout_process.h" // Payments::CheckoutProcess #include "payments/payments_checkout_process.h" // Payments::CheckoutProcess
#include "payments/payments_form.h" // Payments::InvoicePremiumGiftCode #include "payments/payments_form.h" // Payments::InvoicePremiumGiftCode
#include "settings/settings_common.h" #include "settings/settings_common.h"
#include "settings/settings_premium.h" // Settings::ShowPremium #include "settings/settings_premium.h" // Settings::ShowPremium
#include "ui/boxes/choose_date_time.h" #include "ui/boxes/choose_date_time.h"
#include "ui/boxes/confirm_box.h" #include "ui/boxes/confirm_box.h"
#include "ui/effects/credits_graphics.h"
#include "ui/effects/premium_graphics.h" #include "ui/effects/premium_graphics.h"
#include "ui/effects/premium_top_bar.h" #include "ui/effects/premium_top_bar.h"
#include "ui/layers/generic_box.h" #include "ui/layers/generic_box.h"
#include "ui/painter.h" #include "ui/painter.h"
#include "ui/rect.h" #include "ui/rect.h"
#include "ui/vertical_list.h"
#include "ui/text/format_values.h" #include "ui/text/format_values.h"
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/toast/toast.h" #include "ui/toast/toast.h"
#include "ui/vertical_list.h"
#include "ui/widgets/checkbox.h" #include "ui/widgets/checkbox.h"
#include "ui/widgets/continuous_sliders.h" #include "ui/widgets/continuous_sliders.h"
#include "ui/widgets/fields/input_field.h" #include "ui/widgets/fields/input_field.h"
#include "ui/widgets/labels.h" #include "ui/widgets/labels.h"
#include "ui/wrap/slide_wrap.h" #include "ui/wrap/slide_wrap.h"
#include "ui/ui_utility.h" #include "ui/ui_utility.h"
#include "styles/style_credits.h"
#include "styles/style_giveaway.h" #include "styles/style_giveaway.h"
#include "styles/style_info.h" #include "styles/style_info.h"
#include "styles/style_layers.h" #include "styles/style_layers.h"
#include "styles/style_premium.h" #include "styles/style_premium.h"
#include "styles/style_settings.h" #include "styles/style_settings.h"
#include "styles/style_statistics.h"
#include <xxhash.h> // XXH64.
namespace { namespace {
@ -64,6 +71,19 @@ constexpr auto kAdditionalPrizeLengthMax = 128;
return dateNow; return dateNow;
} }
[[nodiscard]] uint64 UniqueIdFromCreditsOption(
const Data::CreditsGiveawayOption &d,
not_null<PeerData*> peer) {
const auto string = QString::number(d.credits)
+ d.storeProduct
+ d.currency
+ QString::number(d.amount)
+ QString::number(peer->id.value)
+ QString::number(peer->session().uniqueId());
return XXH64(string.data(), string.size() * sizeof(ushort), 0);
}
[[nodiscard]] Fn<bool(int)> CreateErrorCallback( [[nodiscard]] Fn<bool(int)> CreateErrorCallback(
int max, int max,
tr::phrase<lngtag_count> phrase) { tr::phrase<lngtag_count> phrase) {
@ -249,6 +269,7 @@ void CreateGiveawayBox(
using GiveawayType = Giveaway::GiveawayTypeRow::Type; using GiveawayType = Giveaway::GiveawayTypeRow::Type;
using GiveawayGroup = Ui::RadioenumGroup<GiveawayType>; using GiveawayGroup = Ui::RadioenumGroup<GiveawayType>;
using CreditsGroup = Ui::RadioenumGroup<int>;
struct State final { struct State final {
State(not_null<PeerData*> p) : apiOptions(p), apiCreditsOptions(p) { State(not_null<PeerData*> p) : apiOptions(p), apiCreditsOptions(p) {
} }
@ -276,6 +297,7 @@ void CreateGiveawayBox(
const auto group = peer->isMegagroup(); const auto group = peer->isMegagroup();
const auto state = box->lifetime().make_state<State>(peer); const auto state = box->lifetime().make_state<State>(peer);
const auto typeGroup = std::make_shared<GiveawayGroup>(); const auto typeGroup = std::make_shared<GiveawayGroup>();
const auto creditsGroup = std::make_shared<CreditsGroup>();
auto showFinished = Ui::BoxShowFinishes(box); auto showFinished = Ui::BoxShowFinishes(box);
AddPremiumTopBarWithDefaultTitleBar( AddPremiumTopBarWithDefaultTitleBar(
@ -346,6 +368,20 @@ void CreateGiveawayBox(
state->typeValue.force_assign(GiveawayType::Random); state->typeValue.force_assign(GiveawayType::Random);
}); });
} }
const auto creditsOption = [=](int index) {
const auto options = state->apiCreditsOptions.options();
return (index >= 0 && index < options.size())
? options[index]
: Data::CreditsGiveawayOption();
};
const auto creditsOptionWinners = [=](int index) {
const auto winners = creditsOption(index).winners;
return ranges::views::all(
winners
) | ranges::views::transform([](const auto &w) {
return w.users;
}) | ranges::to_vector;
};
const auto creditsTypeWrap = contentWrap->entity()->add( const auto creditsTypeWrap = contentWrap->entity()->add(
object_ptr<Ui::VerticalLayout>(contentWrap->entity())); object_ptr<Ui::VerticalLayout>(contentWrap->entity()));
const auto fillCreditsTypeWrap = [=] { const auto fillCreditsTypeWrap = [=] {
@ -467,28 +503,241 @@ void CreateGiveawayBox(
randomWrap->toggleOn( randomWrap->toggleOn(
state->typeValue.value( state->typeValue.value(
) | rpl::map(rpl::mappers::_1 == GiveawayType::Random), ) | rpl::map((rpl::mappers::_1 == GiveawayType::Random)
|| (rpl::mappers::_1 == GiveawayType::Credits)),
anim::type::instant); anim::type::instant);
const auto sliderContainer = randomWrap->entity()->add( const auto randomCreditsWrap = randomWrap->entity()->add(
object_ptr<Ui::VerticalLayout>(randomWrap)); object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
contentWrap,
object_ptr<Ui::VerticalLayout>(box)));
randomCreditsWrap->toggleOn(
state->typeValue.value(
) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits),
anim::type::instant);
const auto fillCreditsOptions = [=] {
randomCreditsWrap->entity()->clear();
const auto &st = st::giveawayTypeListItem;
const auto &stButton = st::defaultSettingsButton;
const auto &stStatus = st::defaultTextStyle;
const auto buttonInnerSkip = st.height - stButton.height;
const auto options = state->apiCreditsOptions.options();
const auto singleStarWidth = Ui::GenerateStars(
st.nameStyle.font->height,
1).width() / style::DevicePixelRatio();
const auto content = randomCreditsWrap->entity();
const auto title = Ui::AddSubsectionTitle(
content,
tr::lng_giveaway_credits_options_title());
const auto rightLabel = Ui::CreateChild<Ui::FlatLabel>(
content,
st::giveawayGiftCodeQuantitySubtitle);
rightLabel->show();
rpl::combine(
tr::lng_giveaway_quantity(
lt_count,
creditsGroup->value() | rpl::map([=](int i) -> float64 {
return creditsOption(i).yearlyBoosts;
})),
title->positionValue(),
content->geometryValue()
) | rpl::start_with_next([=](QString s, const QPoint &p, QRect) {
rightLabel->setText(std::move(s));
rightLabel->moveToRight(st::boxRowPadding.right(), p.y());
}, rightLabel->lifetime());
const auto buttonHeight = st.height;
const auto minCredits = 0;
struct State final {
rpl::variable<bool> isExtended = false;
};
const auto creditsState = content->lifetime().make_state<State>();
for (auto i = 0; i < options.size(); i++) {
const auto &option = options[i];
if (option.credits < minCredits) {
continue;
}
struct State final {
std::optional<Ui::Text::String> text;
QString status;
bool hasStatus = false;
};
const auto buttonWrap = content->add(
object_ptr<Ui::SlideWrap<Ui::SettingsButton>>(
content,
object_ptr<Ui::SettingsButton>(
content,
rpl::never<QString>(),
stButton)));
const auto button = buttonWrap->entity();
button->setPaddingOverride({ 0, buttonInnerSkip, 0, 0 });
const auto buttonState = button->lifetime().make_state<State>();
buttonState->text.emplace(
st.nameStyle,
tr::lng_credits_summary_options_credits(
tr::now,
lt_count_decimal,
option.credits));
buttonState->status = tr::lng_giveaway_credits_option_status(
tr::now,
lt_count_decimal,
option.credits);
const auto price = Ui::CreateChild<Ui::FlatLabel>(
button,
Ui::FillAmountAndCurrency(option.amount, option.currency),
st::creditsTopupPrice);
const auto inner = Ui::CreateChild<Ui::RpWidget>(button);
const auto stars = Ui::GenerateStars(
st.nameStyle.font->height,
(i + 1));
const auto textLeft = st.photoPosition.x()
+ (st.nameStyle.font->spacew * 2)
+ (stars.width() / style::DevicePixelRatio());
state->sliderValue.value(
) | rpl::start_with_next([=](int users) {
const auto option = creditsOption(i);
buttonState->hasStatus = false;
for (const auto &winner : option.winners) {
if (winner.users == users) {
auto status = tr::lng_giveaway_credits_option_status(
tr::now,
lt_count_decimal,
winner.perUserStars);
buttonState->status = std::move(status);
buttonState->hasStatus = true;
inner->update();
return;
}
}
inner->update();
}, button->lifetime());
inner->paintRequest(
) | rpl::start_with_next([=](const QRect &rect) {
auto p = QPainter(inner);
const auto namey = buttonState->hasStatus
? st.namePosition.y()
: (buttonHeight - stStatus.font->height) / 2;
p.drawImage(st.photoPosition.x(), namey, stars);
p.setPen(st.nameFg);
buttonState->text->draw(p, {
.position = QPoint(textLeft, namey),
.availableWidth = inner->width() - textLeft,
.elisionLines = 1,
});
if (buttonState->hasStatus) {
p.setFont(stStatus.font);
p.setPen(st.statusFg);
p.setBrush(Qt::NoBrush);
p.drawText(
st.photoPosition.x(),
st.statusPosition.y() + stStatus.font->ascent,
buttonState->status);
}
}, inner->lifetime());
button->widthValue(
) | rpl::start_with_next([=](int width) {
price->moveToRight(
st::boxRowPadding.right(),
(buttonHeight - price->height()) / 2);
inner->moveToLeft(0, 0);
inner->resize(
width
- price->width()
- st::boxRowPadding.right()
- st::boxRowPadding.left() / 2,
buttonHeight);
}, button->lifetime());
{
const auto &st = st::defaultCheckbox;
const auto radio = Ui::CreateChild<Ui::Radioenum<int>>(
button,
creditsGroup,
i,
QString(),
st);
radio->moveToLeft(
st::boxRowPadding.left(),
(buttonHeight - radio->checkRect().height()) / 2);
radio->setAttribute(Qt::WA_TransparentForMouseEvents);
radio->show();
}
button->setClickedCallback([=] {
creditsGroup->setValue(i);
});
if (option.isDefault) {
creditsGroup->setValue(i);
}
buttonWrap->toggle(
(!option.isExtended) || option.isDefault,
anim::type::instant);
if (option.isExtended) {
buttonWrap->toggleOn(creditsState->isExtended.value());
}
Ui::ToggleChildrenVisibility(button, true);
}
{
Ui::AddSkip(content, st::settingsButton.padding.top());
const auto showMoreWrap = Info::Statistics::AddShowMoreButton(
content,
tr::lng_stories_show_more());
showMoreWrap->toggle(true, anim::type::instant);
showMoreWrap->entity()->setClickedCallback([=] {
showMoreWrap->toggle(false, anim::type::instant);
creditsState->isExtended = true;
});
}
Ui::AddSkip(content);
Ui::AddDividerText(content, tr::lng_giveaway_credits_options_about());
Ui::AddSkip(content);
};
const auto sliderContainerWrap = randomWrap->entity()->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
randomWrap,
object_ptr<Ui::VerticalLayout>(randomWrap)));
const auto sliderContainer = sliderContainerWrap->entity();
sliderContainerWrap->toggle(true, anim::type::instant);
const auto fillSliderContainer = [=] { const auto fillSliderContainer = [=] {
const auto availablePresets = state->apiOptions.availablePresets(); const auto availablePresets = state->apiOptions.availablePresets();
const auto creditsOptions = state->apiCreditsOptions.options();
if (prepaid) { if (prepaid) {
state->sliderValue = prepaid->quantity; state->sliderValue = prepaid->quantity;
return; return;
} }
if (availablePresets.empty()) { if (availablePresets.empty()
&& (creditsOptions.empty()
|| creditsOptions.front().winners.empty())) {
return; return;
} }
state->sliderValue = availablePresets.front(); state->sliderValue = availablePresets.empty()
? creditsOptions.front().winners.front().users
: availablePresets.front();
auto creditsValueType = typeGroup->value(
) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits);
const auto title = Ui::AddSubsectionTitle( const auto title = Ui::AddSubsectionTitle(
sliderContainer, sliderContainer,
tr::lng_giveaway_quantity_title()); rpl::conditional(
rpl::duplicate(creditsValueType),
tr::lng_giveaway_credits_quantity_title(),
tr::lng_giveaway_quantity_title()));
const auto rightLabel = Ui::CreateChild<Ui::FlatLabel>( const auto rightLabel = Ui::CreateChild<Ui::FlatLabel>(
sliderContainer, sliderContainer,
st::giveawayGiftCodeQuantitySubtitle); st::giveawayGiftCodeQuantitySubtitle);
rightLabel->show(); rightLabel->show();
rpl::duplicate(
creditsValueType
) | rpl::start_with_next([=](bool isCredits) {
rightLabel->setVisible(!isCredits);
}, rightLabel->lifetime());
const auto floatLabel = Ui::CreateChild<Ui::FlatLabel>( const auto floatLabel = Ui::CreateChild<Ui::FlatLabel>(
sliderContainer, sliderContainer,
@ -512,37 +761,84 @@ void CreateGiveawayBox(
const auto &padding = st::giveawayGiftCodeSliderPadding; const auto &padding = st::giveawayGiftCodeSliderPadding;
Ui::AddSkip(sliderContainer, padding.top()); Ui::AddSkip(sliderContainer, padding.top());
const auto slider = sliderContainer->add( const auto sliderParent = sliderContainer->add(
object_ptr<Ui::MediaSliderWheelless>( object_ptr<Ui::VerticalLayout>(sliderContainer),
sliderContainer,
st::settingsScale),
st::boxRowPadding); st::boxRowPadding);
struct State final {
Ui::MediaSliderWheelless *slider = nullptr;
};
const auto sliderState = sliderParent->lifetime().make_state<State>();
Ui::AddSkip(sliderContainer, padding.bottom()); Ui::AddSkip(sliderContainer, padding.bottom());
slider->resize(slider->width(), st::settingsScale.seekSize.height()); rpl::combine(
slider->setPseudoDiscrete( rpl::duplicate(creditsValueType),
availablePresets.size(), creditsGroup->value()
[=](int index) { return availablePresets[index]; }, ) | rpl::start_with_next([=](bool isCredits, int value) {
availablePresets.front(), while (sliderParent->count()) {
[=](int boosts) { state->sliderValue = boosts; }, delete sliderParent->widgetAt(0);
[](int) {}); }
sliderState->slider = sliderParent->add(
object_ptr<Ui::MediaSliderWheelless>(
sliderContainer,
st::settingsScale));
sliderState->slider->resize(
sliderState->slider->width(),
st::settingsScale.seekSize.height());
const auto &values = isCredits
? creditsOptionWinners(value)
: availablePresets;
const auto resultValue = [&] {
const auto sliderValue = state->sliderValue.current();
return ranges::contains(values, sliderValue)
? sliderValue
: values.front();
}();
state->sliderValue.force_assign(resultValue);
if (values.size() <= 1) {
sliderContainerWrap->toggle(false, anim::type::instant);
return;
} else {
sliderContainerWrap->toggle(true, anim::type::instant);
}
sliderState->slider->setPseudoDiscrete(
values.size(),
[=](int index) { return values[index]; },
resultValue,
[=](int boosts) { state->sliderValue = boosts; },
[](int) {});
}, sliderParent->lifetime());
state->sliderValue.value( rpl::combine(
) | rpl::start_with_next([=](int boosts) { rpl::duplicate(creditsValueType),
creditsGroup->value(),
state->sliderValue.value()
) | rpl::start_with_next([=](
bool isCredits,
int credits,
int boosts) {
floatLabel->setText(QString::number(boosts)); floatLabel->setText(QString::number(boosts));
const auto count = availablePresets.size(); if (!sliderState->slider) {
const auto sliderWidth = slider->width() return;
}
const auto &values = isCredits
? creditsOptionWinners(credits)
: availablePresets;
const auto count = values.size();
const auto sliderWidth = sliderState->slider->width()
- st::settingsScale.seekSize.width(); - st::settingsScale.seekSize.width();
for (auto i = 0; i < count; i++) { for (auto i = 0; i < count; i++) {
if ((i + 1 == count || availablePresets[i + 1] > boosts) if ((i + 1 == count || values[i + 1] > boosts)
&& availablePresets[i] <= boosts) { && values[i] <= boosts) {
const auto x = (sliderWidth * i) / (count - 1); const auto x = (sliderWidth * i) / (count - 1);
const auto mapped = sliderState->slider->mapTo(
sliderContainer,
sliderState->slider->pos());
floatLabel->moveToLeft( floatLabel->moveToLeft(
slider->x() mapped.x()
+ x + x
+ st::settingsScale.seekSize.width() / 2 + st::settingsScale.seekSize.width() / 2
- floatLabel->width() / 2, - floatLabel->width() / 2,
slider->y() mapped.y()
- floatLabel->height() - floatLabel->height()
- st::giveawayGiftCodeSliderFloatSkip); - st::giveawayGiftCodeSliderFloatSkip);
break; break;
@ -553,7 +849,10 @@ void CreateGiveawayBox(
Ui::AddSkip(sliderContainer); Ui::AddSkip(sliderContainer);
Ui::AddDividerText( Ui::AddDividerText(
sliderContainer, sliderContainer,
tr::lng_giveaway_quantity_about()); rpl::conditional(
rpl::duplicate(creditsValueType),
tr::lng_giveaway_credits_quantity_about(),
tr::lng_giveaway_quantity_about()));
Ui::AddSkip(sliderContainer); Ui::AddSkip(sliderContainer);
sliderContainer->resizeToWidth(box->width()); sliderContainer->resizeToWidth(box->width());
@ -1006,13 +1305,18 @@ void CreateGiveawayBox(
button, button,
rpl::conditional( rpl::conditional(
state->typeValue.value( state->typeValue.value(
) | rpl::map(rpl::mappers::_1 == GiveawayType::Random), ) | rpl::map(rpl::mappers::_1 != GiveawayType::Random),
tr::lng_giveaway_start(), tr::lng_giveaway_award(),
tr::lng_giveaway_award()), tr::lng_giveaway_start()),
state->sliderValue.value( rpl::conditional(
) | rpl::map([=](int v) -> int { state->typeValue.value(
return state->apiOptions.giveawayBoostsPerPremium() * v; ) | rpl::map(rpl::mappers::_1 == GiveawayType::Credits),
}), creditsGroup->value() | rpl::map([=](int v) {
return creditsOption(v).yearlyBoosts;
}),
state->sliderValue.value() | rpl::map([=](int v) -> int {
return state->apiOptions.giveawayBoostsPerPremium() * v;
})),
state->confirmButtonBusy.value() | rpl::map(!rpl::mappers::_1)); state->confirmButtonBusy.value() | rpl::map(!rpl::mappers::_1));
{ {
@ -1037,17 +1341,32 @@ void CreateGiveawayBox(
const auto type = typeGroup->current(); const auto type = typeGroup->current();
const auto isSpecific = (type == GiveawayType::SpecificUsers); const auto isSpecific = (type == GiveawayType::SpecificUsers);
const auto isRandom = (type == GiveawayType::Random); const auto isRandom = (type == GiveawayType::Random);
if (!isSpecific && !isRandom) { const auto isCredits = (type == GiveawayType::Credits);
if (!isSpecific && !isRandom && !isCredits) {
return; return;
} }
auto invoice = state->apiOptions.invoice( auto invoice = [&] {
isSpecific if (isCredits) {
? state->selectedToAward.size() const auto option = creditsOption(
: state->sliderValue.current(), creditsGroup->current());
prepaid return Payments::InvoicePremiumGiftCode{
? prepaid->months .currency = option.currency,
: state->apiOptions.monthsFromPreset( .storeProduct = option.storeProduct,
durationGroup->current())); .creditsAmount = option.credits,
.randomId = UniqueIdFromCreditsOption(option, peer),
.amount = option.amount,
.users = state->sliderValue.current(),
};
}
return state->apiOptions.invoice(
isSpecific
? state->selectedToAward.size()
: state->sliderValue.current(),
prepaid
? prepaid->months
: state->apiOptions.monthsFromPreset(
durationGroup->current()));
}();
if (isSpecific) { if (isSpecific) {
if (state->selectedToAward.empty()) { if (state->selectedToAward.empty()) {
return; return;
@ -1061,7 +1380,7 @@ void CreateGiveawayBox(
}) | ranges::to_vector, }) | ranges::to_vector,
peer->asChannel(), peer->asChannel(),
}; };
} else if (isRandom) { } else if (isRandom || isCredits) {
invoice.purpose = Payments::InvoicePremiumGiftCodeGiveaway{ invoice.purpose = Payments::InvoicePremiumGiftCodeGiveaway{
.boostPeer = peer->asChannel(), .boostPeer = peer->asChannel(),
.additionalChannels = ranges::views::all( .additionalChannels = ranges::views::all(
@ -1180,6 +1499,7 @@ void CreateGiveawayBox(
state->chosenMonths = state->apiOptions.monthsFromPreset(0); state->chosenMonths = state->apiOptions.monthsFromPreset(0);
} }
fillCreditsTypeWrap(); fillCreditsTypeWrap();
fillCreditsOptions();
rebuildListOptions(state->typeValue.current(), 1); rebuildListOptions(state->typeValue.current(), 1);
contentWrap->toggle(true, anim::type::instant); contentWrap->toggle(true, anim::type::instant);
contentWrap->resizeToWidth(box->width()); contentWrap->resizeToWidth(box->width());