From 02610de010e24e04bb50235a29cec8673b4ba6b0 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 7 Aug 2024 18:09:32 +0200 Subject: [PATCH] Improve paid reactions box design. --- Telegram/Resources/langs/lang.strings | 4 +- .../SourceFiles/boxes/premium_limits_box.cpp | 11 +- .../payments/payments_reaction_process.cpp | 26 +- .../payments/ui/payments_reaction_box.cpp | 178 +++++- .../payments/ui/payments_reaction_box.h | 4 +- Telegram/SourceFiles/ui/boxes/boost_box.cpp | 3 +- Telegram/SourceFiles/ui/effects/premium.style | 33 +- .../SourceFiles/ui/effects/premium_bubble.cpp | 475 ++++++++++++++ .../SourceFiles/ui/effects/premium_bubble.h | 168 +++++ .../ui/effects/premium_graphics.cpp | 578 +----------------- .../SourceFiles/ui/effects/premium_graphics.h | 32 +- Telegram/cmake/td_ui.cmake | 2 + Telegram/lib_ui | 2 +- 13 files changed, 875 insertions(+), 641 deletions(-) create mode 100644 Telegram/SourceFiles/ui/effects/premium_bubble.cpp create mode 100644 Telegram/SourceFiles/ui/effects/premium_bubble.h diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index eb34388d8..13cab0bee 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3419,7 +3419,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_paid_price" = "Unlock for {price}"; "lng_paid_react_title" = "Star Reaction"; -"lng_paid_react_about" = "Choose how many stars you want to send to {channel} to support this post."; +"lng_paid_react_about" = "Choose how many **Stars** you want to send to {channel} to support this post."; +"lng_paid_react_already#one" = "You sent **{count} Star** to support this post."; +"lng_paid_react_already#other" = "You sent **{count} Stars** to support this post."; "lng_paid_react_top_title" = "Top Senders"; "lng_paid_react_send" = "Send {price}"; "lng_paid_react_agree" = "By sending stars, you agree to the {link}."; diff --git a/Telegram/SourceFiles/boxes/premium_limits_box.cpp b/Telegram/SourceFiles/boxes/premium_limits_box.cpp index 8febb5f42..325e92c44 100644 --- a/Telegram/SourceFiles/boxes/premium_limits_box.cpp +++ b/Telegram/SourceFiles/boxes/premium_limits_box.cpp @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/boxes/confirm_box.h" #include "ui/controls/peer_list_dummy.h" +#include "ui/effects/premium_bubble.h" #include "ui/effects/premium_graphics.h" #include "ui/widgets/checkbox.h" #include "ui/wrap/padding_wrap.h" @@ -136,6 +137,12 @@ private: }; +[[nodiscard]] Ui::Premium::BubbleType ChooseBubbleType(bool premium) { + return premium + ? Ui::Premium::BubbleType::Premium + : Ui::Premium::BubbleType::NoPremium; +} + void InactiveDelegate::peerListSetTitle(rpl::producer title) { } @@ -421,7 +428,7 @@ void SimpleLimitBox( (descriptor.complexRatio ? descriptor.premiumLimit : 2 * descriptor.current), - premiumPossible, + ChooseBubbleType(premiumPossible), descriptor.phrase, descriptor.icon); Ui::AddSkip(top, st::premiumLineTextSkip); @@ -1109,7 +1116,7 @@ void AccountsLimitBox( : (current > defaultLimit) ? (current + 1) : (defaultLimit * 2)), - premiumPossible, + ChooseBubbleType(premiumPossible), std::nullopt, &st::premiumIconAccounts); Ui::AddSkip(top, st::premiumLineTextSkip); diff --git a/Telegram/SourceFiles/payments/payments_reaction_process.cpp b/Telegram/SourceFiles/payments/payments_reaction_process.cpp index dd23c69fe..5e40068b1 100644 --- a/Telegram/SourceFiles/payments/payments_reaction_process.cpp +++ b/Telegram/SourceFiles/payments/payments_reaction_process.cpp @@ -34,7 +34,7 @@ namespace Payments { namespace { constexpr auto kMaxPerReactionFallback = 2'500; -constexpr auto kDefaultPerReaction = 20; +constexpr auto kDefaultPerReaction = 50; void TryAddingPaidReaction( not_null session, @@ -82,6 +82,17 @@ void TryAddingPaidReaction( done); } +[[nodiscard]] int CountLocalPaid(not_null item) { + const auto paid = [](const std::vector &v) { + const auto i = ranges::find( + v, + Data::ReactionId::Paid(), + &Data::MessageReaction::id); + return (i != end(v)) ? i->count : 0; + }; + return paid(item->reactionsWithLocal()) - paid(item->reactions()); +} + } // namespace void TryAddingPaidReaction( @@ -110,13 +121,12 @@ void ShowPaidReactionDetails( const auto session = &item->history()->session(); const auto appConfig = &session->appConfig(); - const auto min = 1; const auto max = std::max( appConfig->get( u"stars_paid_reaction_amount_max"_q, kMaxPerReactionFallback), - min); - const auto chosen = std::clamp(kDefaultPerReaction, min, max); + 2); + const auto chosen = std::clamp(kDefaultPerReaction, 1, max); struct State { QPointer selectBox; @@ -169,10 +179,14 @@ void ShowPaidReactionDetails( }; }); }; + auto already = 0; auto top = std::vector(); const auto &topPaid = item->topPaidReactions(); top.reserve(topPaid.size()); for (const auto &entry : topPaid) { + if (entry.my) { + already = entry.count; + } if (!entry.top) { continue; } @@ -185,9 +199,9 @@ void ShowPaidReactionDetails( ranges::sort(top, ranges::greater(), &Ui::PaidReactionTop::count); state->selectBox = show->show(Ui::MakePaidReactionBox({ - .min = min, - .max = max, + .already = already + CountLocalPaid(item), .chosen = chosen, + .max = max, .top = std::move(top), .channel = item->history()->peer->name(), .submit = std::move(submitText), diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp index ef5ac8953..af589ed19 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.cpp @@ -9,12 +9,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "ui/boxes/boost_box.h" // MakeBoostFeaturesBadge. +#include "ui/effects/premium_bubble.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" #include "ui/widgets/buttons.h" #include "ui/widgets/continuous_sliders.h" #include "ui/dynamic_image.h" #include "ui/painter.h" +#include "ui/vertical_list.h" #include "styles/style_chat.h" #include "styles/style_credits.h" #include "styles/style_layers.h" @@ -33,23 +35,94 @@ namespace { constexpr auto kMaxTopPaidShown = 3; +struct Discreter { + Fn ratioToValue; + Fn valueToRatio; +}; + +[[nodiscard]] Discreter DiscreterForMax(int max) { + Expects(max >= 2); + + // 1/8 of width is 1..10 + // 1/3 of width is 1..100 + // 2/3 of width is 1..1000 + + auto thresholds = base::flat_map(); + thresholds.emplace(0., 1); + if (max <= 40) { + thresholds.emplace(1., max); + } else if (max <= 300) { + thresholds.emplace(1. / 4, 10); + thresholds.emplace(1., max); + } else if (max <= 600) { + thresholds.emplace(1. / 8, 10); + thresholds.emplace(1. / 2, 100); + thresholds.emplace(1., max); + } else if (max <= 1900) { + thresholds.emplace(1. / 8, 10); + thresholds.emplace(1. / 3, 100); + thresholds.emplace(1., max); + } else { + thresholds.emplace(1. / 8, 10); + thresholds.emplace(1. / 3, 100); + thresholds.emplace(2. / 3, 1000); + thresholds.emplace(1., max); + } + + const auto ratioToValue = [=](float64 ratio) { + ratio = std::clamp(ratio, 0., 1.); + const auto j = thresholds.lower_bound(ratio); + if (j == begin(thresholds)) { + return 1; + } + const auto i = j - 1; + const auto progress = (ratio - i->first) / (j->first - i->first); + const auto value = i->second + (j->second - i->second) * progress; + return int(base::SafeRound(value)); + }; + const auto valueToRatio = [=](int value) { + value = std::clamp(value, 1, max); + auto i = begin(thresholds); + auto j = i + 1; + while (j->second < value) { + i = j++; + } + const auto progress = (value - i->second) + / float64(j->second - i->second); + return i->first + (j->first - i->first) * progress; + }; + return { + .ratioToValue = ratioToValue, + .valueToRatio = valueToRatio, + }; +} + void PaidReactionSlider( not_null container, - int min, int current, int max, Fn changed) { - const auto top = st::boxTitleClose.height + st::creditsHistoryRightSkip; + Expects(current >= 1 && current <= max); + const auto slider = container->add( object_ptr(container, st::paidReactSlider), - st::boxRowPadding + QMargins(0, top, 0, 0)); + st::boxRowPadding + QMargins(0, st::paidReactSliderTop, 0, 0)); slider->resize(slider->width(), st::paidReactSlider.seekSize.height()); - slider->setPseudoDiscrete( - max + 1 - min, - [=](int index) { return min + index; }, - current - min, - changed, - changed); + + const auto discreter = DiscreterForMax(max); + slider->setAlwaysDisplayMarker(true); + slider->setDirection(ContinuousSlider::Direction::Horizontal); + slider->setValue(discreter.valueToRatio(current)); + slider->setAdjustCallback([=](float64 ratio) { + return discreter.valueToRatio(discreter.ratioToValue(ratio)); + }); + const auto ratioToValue = discreter.ratioToValue; + slider->setChangeProgressCallback([=](float64 value) { + changed(ratioToValue(value)); + }); + slider->setChangeFinishedCallback([=](float64 value) { + changed(ratioToValue(value)); + }); } [[nodiscard]] QImage GenerateBadgeImage(int count) { @@ -100,15 +173,15 @@ void PaidReactionSlider( return result; } -[[nodiscard]] not_null MakeTopReactor( +[[nodiscard]] not_null MakeTopReactor( not_null parent, const PaidReactionTop &data) { - const auto result = Ui::CreateChild(parent); + const auto result = CreateChild(parent); result->show(); struct State { QImage badge; - Ui::Text::String name; + Text::String name; }; const auto state = result->lifetime().make_state(); state->name.setText(st::defaultTextStyle, data.name); @@ -159,10 +232,10 @@ void FillTopReactors( const auto height = st::paidReactTopNameSkip + st::normalFont->height; const auto wrap = container->add( - object_ptr(container, height), + object_ptr(container, height), st::paidReactTopMargin); struct State { - std::vector> widgets; + std::vector> widgets; }; const auto state = wrap->lifetime().make_state(); @@ -189,6 +262,9 @@ void FillTopReactors( void PaidReactionsBox( not_null box, PaidReactionBoxArgs &&args) { + args.max = std::max(args.max, 2); + args.chosen = std::clamp(args.chosen, 1, args.max); + box->setWidth(st::boxWideWidth); box->setStyle(st::paidReactBox); box->setNoContentMargin(true); @@ -201,41 +277,79 @@ void PaidReactionsBox( const auto changed = [=](int count) { state->chosen = count; }; - PaidReactionSlider( - box->verticalLayout(), - args.min, - args.chosen, - args.max, - changed); + + const auto content = box->verticalLayout(); + AddSkip(content, st::boxTitleClose.height + st::paidReactBubbleTop); + + const auto valueToRatio = DiscreterForMax(args.max).valueToRatio; + auto bubbleRowState = state->chosen.value() | rpl::map([=](int value) { + const auto full = st::boxWideWidth + - st::boxRowPadding.left() + - st::boxRowPadding.right(); + const auto marker = st::paidReactSlider.seekSize.width(); + const auto start = marker / 2; + const auto inner = full - marker; + const auto correct = start + inner * valueToRatio(value); + return Premium::BubbleRowState{ + .counter = value, + .ratio = correct / full, + }; + }); + Premium::AddBubbleRow( + content, + st::boostBubble, + BoxShowFinishes(box), + std::move(bubbleRowState), + Premium::BubbleType::Credits, + nullptr, + &st::paidReactBubbleIcon, + st::boxRowPadding); + + PaidReactionSlider(content, args.chosen, args.max, changed); box->addTopButton(st::boxTitleClose, [=] { box->closeBox(); }); box->addRow( - object_ptr( + object_ptr( box, tr::lng_paid_react_title(), st::boostCenteredTitle), st::boxRowPadding + QMargins(0, st::paidReactTitleSkip, 0, 0)); - box->addRow( - object_ptr( - box, - tr::lng_paid_react_about( - lt_channel, - rpl::single(Text::Bold(args.channel)), - Text::RichLangValue), - st::boostText), + const auto labelWrap = box->addRow( + object_ptr(box), (st::boxRowPadding + QMargins(0, st::lineWidth, 0, st::boostBottomSkip))); + const auto label = CreateChild( + labelWrap, + (args.already + ? tr::lng_paid_react_already( + lt_count, + rpl::single(args.already) | tr::to_count(), + Text::RichLangValue) + : tr::lng_paid_react_about( + lt_channel, + rpl::single(Text::Bold(args.channel)), + Text::RichLangValue)), + st::boostText); + labelWrap->widthValue() | rpl::start_with_next([=](int width) { + label->resizeToWidth(width); + }, label->lifetime()); + label->heightValue() | rpl::start_with_next([=](int height) { + const auto min = 2 * st::normalFont->height; + const auto skip = std::max((min - height) / 2, 0); + labelWrap->resize(labelWrap->width(), 2 * skip + height); + label->moveToLeft(0, skip); + }, label->lifetime()); if (!args.top.empty()) { - FillTopReactors(box->verticalLayout(), std::move(args.top)); + FillTopReactors(content, std::move(args.top)); } const auto button = box->addButton(rpl::single(QString()), [=] { args.send(state->chosen.current()); }); { - const auto buttonLabel = Ui::CreateChild( + const auto buttonLabel = CreateChild( button, rpl::single(QString()), st::creditsBoxButtonLabel); @@ -268,7 +382,7 @@ void PaidReactionsBox( { const auto balance = Settings::AddBalanceWidget( - box->verticalLayout(), + content, std::move(args.balanceValue), false); rpl::combine( diff --git a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h index 260896c9a..7b830c974 100644 --- a/Telegram/SourceFiles/payments/ui/payments_reaction_box.h +++ b/Telegram/SourceFiles/payments/ui/payments_reaction_box.h @@ -27,9 +27,9 @@ struct PaidReactionTop { }; struct PaidReactionBoxArgs { - int min = 0; - int max = 0; + int already = 0; int chosen = 0; + int max = 0; std::vector top; diff --git a/Telegram/SourceFiles/ui/boxes/boost_box.cpp b/Telegram/SourceFiles/ui/boxes/boost_box.cpp index 903cdcba7..500748ac2 100644 --- a/Telegram/SourceFiles/ui/boxes/boost_box.cpp +++ b/Telegram/SourceFiles/ui/boxes/boost_box.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "lang/lang_keys.h" #include "ui/boxes/confirm_box.h" #include "ui/effects/fireworks_animation.h" +#include "ui/effects/premium_bubble.h" #include "ui/effects/premium_graphics.h" #include "ui/layers/generic_box.h" #include "ui/text/text_utilities.h" @@ -811,7 +812,7 @@ void FillBoostLimit( st::boostBubble, std::move(showFinished), rpl::duplicate(bubbleRowState), - true, + Premium::BubbleType::Premium, nullptr, &st::premiumIconBoost, limitLinePadding); diff --git a/Telegram/SourceFiles/ui/effects/premium.style b/Telegram/SourceFiles/ui/effects/premium.style index 1c7b8884c..ba85887ff 100644 --- a/Telegram/SourceFiles/ui/effects/premium.style +++ b/Telegram/SourceFiles/ui/effects/premium.style @@ -366,12 +366,18 @@ boostFeatureStories: icon{{ "settings/premium/features/feature_stories", windowB boostFeatureTranscribe: icon{{ "settings/premium/features/feature_voice", windowBgActive }}; boostFeatureOffSponsored: icon{{ "settings/premium/features/feature_off_sponsored", windowBgActive }}; -paidReactTitleSkip: 23px; -paidReactTopTitleMargin: margins(10px, 26px, 10px, 12px); -paidReactTopMargin: margins(0px, 12px, 0px, 11px); -paidReactTopUserpic: 42px; -paidReactTopNameSkip: 47px; -paidReactTopBadgeSkip: 32px; +paidReactBox: Box(boostBox) { + buttonPadding: margins(22px, 22px, 22px, 22px); + buttonHeight: 42px; + button: RoundButton(defaultActiveButton) { + height: 42px; + textTop: 12px; + font: font(13px semibold); + } +} +paidReactBubbleIcon: icon{{ "settings/premium/star", premiumButtonFg }}; +paidReactBubbleTop: 5px; +paidReactSliderTop: 5px; paidReactSlider: MediaSlider(defaultContinuousSlider) { activeFg: creditsBg3; inactiveFg: creditsBg2; @@ -382,12 +388,9 @@ paidReactSlider: MediaSlider(defaultContinuousSlider) { width: 6px; seekSize: size(16px, 16px); } -paidReactBox: Box(boostBox) { - buttonPadding: margins(22px, 22px, 22px, 22px); - buttonHeight: 42px; - button: RoundButton(defaultActiveButton) { - height: 42px; - textTop: 12px; - font: font(13px semibold); - } -} +paidReactTitleSkip: 23px; +paidReactTopTitleMargin: margins(10px, 26px, 10px, 12px); +paidReactTopMargin: margins(0px, 12px, 0px, 11px); +paidReactTopUserpic: 42px; +paidReactTopNameSkip: 47px; +paidReactTopBadgeSkip: 32px; diff --git a/Telegram/SourceFiles/ui/effects/premium_bubble.cpp b/Telegram/SourceFiles/ui/effects/premium_bubble.cpp new file mode 100644 index 000000000..5a3698e17 --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/premium_bubble.cpp @@ -0,0 +1,475 @@ +/* +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/effects/premium_bubble.h" + +#include "base/debug_log.h" +#include "base/object_ptr.h" +#include "lang/lang_keys.h" +#include "ui/effects/gradient.h" +#include "ui/effects/premium_graphics.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/painter.h" +#include "styles/style_layers.h" +#include "styles/style_premium.h" + +namespace Ui::Premium { +namespace { + +constexpr auto kBubbleRadiusSubtractor = 2; +constexpr auto kDeflectionSmall = 20.; +constexpr auto kDeflection = 30.; +constexpr auto kStepBeforeDeflection = 0.75; +constexpr auto kStepAfterDeflection = kStepBeforeDeflection ++ (1. - kStepBeforeDeflection) / 2.; +constexpr auto kSlideDuration = crl::time(1000); + +} // namespace + +TextFactory ProcessTextFactory( + std::optional> phrase) { + return phrase + ? TextFactory([=](int n) { return (*phrase)(tr::now, lt_count, n); }) + : TextFactory([=](int n) { return QString::number(n); }); +} + +Bubble::Bubble( + const style::PremiumBubble &st, + Fn updateCallback, + TextFactory textFactory, + const style::icon *icon, + bool hasTail) +: _st(st) +, _updateCallback(std::move(updateCallback)) +, _textFactory(std::move(textFactory)) +, _icon(icon) +, _numberAnimation(_st.font, _updateCallback) +, _height(_st.height + _st.tailSize.height()) +, _textTop((_height - _st.tailSize.height() - _st.font->height) / 2) +, _hasTail(hasTail) { + _numberAnimation.setDisabledMonospace(true); + _numberAnimation.setWidthChangedCallback([=] { + _widthChanges.fire({}); + }); + _numberAnimation.setText(_textFactory(0), 0); + _numberAnimation.finishAnimating(); +} + +crl::time Bubble::SlideNoDeflectionDuration() { + return kSlideDuration * kStepBeforeDeflection; +} + +int Bubble::counter() const { + return _counter; +} + +int Bubble::height() const { + return _height; +} + +int Bubble::bubbleRadius() const { + return (_height - _st.tailSize.height()) / 2 - kBubbleRadiusSubtractor; +} + +int Bubble::filledWidth() const { + return _st.padding.left() + + _icon->width() + + _st.textSkip + + _st.padding.right(); +} + +int Bubble::width() const { + return filledWidth() + _numberAnimation.countWidth(); +} + +int Bubble::countMaxWidth(int maxPossibleCounter) const { + auto numbers = Ui::NumbersAnimation(_st.font, [] {}); + numbers.setDisabledMonospace(true); + numbers.setDuration(0); + numbers.setText(_textFactory(0), 0); + numbers.setText(_textFactory(maxPossibleCounter), maxPossibleCounter); + numbers.finishAnimating(); + return filledWidth() + numbers.maxWidth(); +} + +void Bubble::setCounter(int value) { + if (_counter != value) { + _counter = value; + _numberAnimation.setText(_textFactory(_counter), _counter); + } +} + +void Bubble::setTailEdge(EdgeProgress edge) { + _tailEdge = std::clamp(edge, 0., 1.); +} + +void Bubble::setFlipHorizontal(bool value) { + _flipHorizontal = value; +} + +void Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) { + if (_counter < 0) { + return; + } + + const auto penWidth = _st.penWidth; + const auto penWidthHalf = penWidth / 2; + const auto bubbleRect = r - style::margins( + penWidthHalf, + penWidthHalf, + penWidthHalf, + _st.tailSize.height() + penWidthHalf); + { + const auto radius = bubbleRadius(); + auto pathTail = QPainterPath(); + + const auto tailWHalf = _st.tailSize.width() / 2.; + const auto progress = _tailEdge; + + const auto tailTop = bubbleRect.y() + bubbleRect.height(); + const auto tailLeftFull = bubbleRect.x() + + (bubbleRect.width() * 0.5) + - tailWHalf; + const auto tailLeft = bubbleRect.x() + + (bubbleRect.width() * 0.5 * (progress + 1.)) + - tailWHalf; + const auto tailCenter = tailLeft + tailWHalf; + const auto tailRight = [&] { + const auto max = bubbleRect.x() + bubbleRect.width(); + const auto right = tailLeft + _st.tailSize.width(); + const auto bottomMax = max - radius; + return (right > bottomMax) + ? std::max(float64(tailCenter), float64(bottomMax)) + : right; + }(); + if (_hasTail) { + pathTail.moveTo(tailLeftFull, tailTop); + pathTail.lineTo(tailLeft, tailTop); + pathTail.lineTo(tailCenter, tailTop + _st.tailSize.height()); + pathTail.lineTo(tailRight, tailTop); + pathTail.lineTo(tailRight, tailTop - radius); + pathTail.moveTo(tailLeftFull, tailTop); + } + auto pathBubble = QPainterPath(); + pathBubble.setFillRule(Qt::WindingFill); + pathBubble.addRoundedRect(bubbleRect, radius, radius); + + auto hq = PainterHighQualityEnabler(p); + p.setPen(QPen( + brush, + penWidth, + Qt::SolidLine, + Qt::RoundCap, + Qt::RoundJoin)); + p.setBrush(brush); + if (_flipHorizontal) { + auto m = QTransform(); + const auto center = QRectF(bubbleRect).center(); + m.translate(center.x(), center.y()); + m.scale(-1., 1.); + m.translate(-center.x(), -center.y()); + p.drawPath(m.map(pathTail + pathBubble)); + } else { + p.drawPath(pathTail + pathBubble); + } + } + p.setPen(st::activeButtonFg); + p.setFont(_st.font); + const auto iconLeft = r.x() + _st.padding.left(); + _icon->paint( + p, + iconLeft, + bubbleRect.y() + (bubbleRect.height() - _icon->height()) / 2, + bubbleRect.width()); + _numberAnimation.paint( + p, + iconLeft + _icon->width() + _st.textSkip, + r.y() + _textTop, + width() / 2); +} + +rpl::producer<> Bubble::widthChanges() const { + return _widthChanges.events(); +} + +BubbleWidget::BubbleWidget( + not_null parent, + const style::PremiumBubble &st, + TextFactory textFactory, + rpl::producer state, + BubbleType type, + rpl::producer<> showFinishes, + const style::icon *icon, + const style::margins &outerPadding) +: RpWidget(parent) +, _st(st) +, _state(std::move(state)) +, _bubble( + _st, + [=] { update(); }, + std::move(textFactory), + icon, + (type != BubbleType::NoPremium)) +, _type(type) +, _outerPadding(outerPadding) +, _deflection(kDeflection) +, _stepBeforeDeflection(kStepBeforeDeflection) +, _stepAfterDeflection(kStepAfterDeflection) { + const auto resizeTo = [=](int w, int h) { + _deflection = (w > _st.widthLimit) + ? kDeflectionSmall + : kDeflection; + _spaceForDeflection = QSize(_st.skip, _st.skip); + resize(QSize(w, h) + 2 * _spaceForDeflection); + }; + + resizeTo(_bubble.width(), _bubble.height()); + _bubble.widthChanges( + ) | rpl::start_with_next([=] { + resizeTo(_bubble.width(), _bubble.height()); + }, lifetime()); + + std::move( + showFinishes + ) | rpl::take(1) | rpl::start_with_next([=] { + _state.value( + ) | rpl::start_with_next([=](BubbleRowState state) { + animateTo(state); + }, lifetime()); + }, lifetime()); +} + +void BubbleWidget::animateTo(BubbleRowState state) { + _maxBubbleWidth = _bubble.countMaxWidth(state.counter); + const auto parent = parentWidget(); + const auto available = parent->width() + - _outerPadding.left() + - _outerPadding.right(); + const auto halfWidth = (_maxBubbleWidth / 2); + const auto computeLeft = [=](float64 pointRatio, float64 animProgress) { + const auto delta = (pointRatio - _animatingFromResultRatio); + const auto center = available + * (_animatingFromResultRatio + delta * animProgress); + return center - halfWidth + _outerPadding.left(); + }; + const auto moveEndPoint = state.ratio; + const auto computeRightEdge = [=] { + return parent->width() + - _outerPadding.right() + - _maxBubbleWidth; + }; + struct Edge final { + float64 goodPointRatio = 0.; + float64 bubbleEdge = 0.; + }; + const auto desiredFinish = computeLeft(moveEndPoint, 1.); + const auto leftEdge = [&]() -> Edge { + const auto edge = _outerPadding.left(); + if (desiredFinish < edge) { + const auto goodPointRatio = float64(halfWidth) / available; + const auto bubbleLeftEdge = (desiredFinish - edge) + / float64(halfWidth); + return { goodPointRatio, bubbleLeftEdge }; + } + return {}; + }(); + const auto rightEdge = [&]() -> Edge { + const auto edge = computeRightEdge(); + if (desiredFinish > edge) { + const auto goodPointRatio = 1. - float64(halfWidth) / available; + const auto bubbleRightEdge = (desiredFinish - edge) + / float64(halfWidth); + return { goodPointRatio, bubbleRightEdge }; + } + return {}; + }(); + const auto finalEdge = (leftEdge.bubbleEdge < 0.) + ? leftEdge.bubbleEdge + : rightEdge.bubbleEdge; + _ignoreDeflection = !_state.current().dynamic && (finalEdge != 0.); + if (_ignoreDeflection) { + _stepBeforeDeflection = 1.; + _stepAfterDeflection = 1.; + } else { + _stepBeforeDeflection = kStepBeforeDeflection; + _stepAfterDeflection = kStepAfterDeflection; + } + const auto resultMoveEndPoint = (finalEdge < 0) + ? leftEdge.goodPointRatio + : (finalEdge > 0) + ? rightEdge.goodPointRatio + : moveEndPoint; + + const auto duration = kSlideDuration + * (_ignoreDeflection ? kStepBeforeDeflection : 1.) + * ((_state.current().ratio < 0.001) ? 0.5 : 1.); + if (state.animateFromZero) { + _animatingFrom.ratio = 0.; + _animatingFrom.counter = 0; + _animatingFromResultRatio = 0.; + _animatingFromBubbleEdge = 0.; + } + _appearanceAnimation.start([=](float64 value) { + if (!_appearanceAnimation.animating()) { + _animatingFrom = state; + _animatingFromResultRatio = resultMoveEndPoint; + _animatingFromBubbleEdge = finalEdge; + } + value = std::abs(value); + const auto moveProgress = std::clamp( + (value / _stepBeforeDeflection), + 0., + 1.); + const auto counterProgress = std::clamp( + (value / _stepAfterDeflection), + 0., + 1.); + const auto nowBubbleEdge = _animatingFromBubbleEdge + + (finalEdge - _animatingFromBubbleEdge) * moveProgress; + moveToLeft(-_spaceForDeflection.width() + + std::max( + int(base::SafeRound( + computeLeft(resultMoveEndPoint, moveProgress))), + 0), + 0); + + const auto now = _animatingFrom.counter + + counterProgress * (state.counter - _animatingFrom.counter); + _bubble.setCounter(int(base::SafeRound(now))); + + _bubble.setFlipHorizontal(nowBubbleEdge < 0); + _bubble.setTailEdge(std::abs(nowBubbleEdge)); + update(); + }, + 0., + (state.ratio >= _animatingFrom.ratio) ? 1. : -1., + duration, + anim::easeOutCirc); +} + +void BubbleWidget::paintEvent(QPaintEvent *e) { + if (_bubble.counter() < 0) { + return; + } + + auto p = QPainter(this); + + const auto padding = QMargins( + _spaceForDeflection.width(), + _spaceForDeflection.height(), + _spaceForDeflection.width(), + _spaceForDeflection.height()); + const auto bubbleRect = rect() - padding; + + const auto params = GradientParams{ + .left = x() + _spaceForDeflection.width(), + .width = bubbleRect.width(), + .outer = parentWidget()->parentWidget()->width(), + }; + if (_cachedGradientParams != params) { + _cachedGradient = ComputeGradient( + parentWidget(), + params.left, + params.width); + _cachedGradientParams = params; + } + if (_appearanceAnimation.animating()) { + const auto value = _appearanceAnimation.value(1.); + const auto progress = std::abs(value); + const auto finalScale = (_animatingFromResultRatio > 0.) + || (_state.current().ratio < 0.001); + const auto scaleProgress = finalScale + ? 1. + : std::clamp((progress / _stepBeforeDeflection), 0., 1.); + const auto scale = scaleProgress; + const auto rotationProgress = std::clamp( + (progress - _stepBeforeDeflection) / (1. - _stepBeforeDeflection), + 0., + 1.); + const auto rotationProgressReverse = std::clamp( + (progress - _stepAfterDeflection) / (1. - _stepAfterDeflection), + 0., + 1.); + + const auto offsetX = bubbleRect.x() + bubbleRect.width() / 2; + const auto offsetY = bubbleRect.y() + bubbleRect.height(); + p.translate(offsetX, offsetY); + p.scale(scale, scale); + if (!_ignoreDeflection) { + p.rotate((rotationProgress - rotationProgressReverse) + * _deflection + * (value < 0. ? -1. : 1.)); + } + p.translate(-offsetX, -offsetY); + } + + + _bubble.paintBubble(p, bubbleRect, [&] { + switch (_type) { + case BubbleType::NoPremium: return st::windowBgActive->b; + case BubbleType::Premium: return QBrush(_cachedGradient); + case BubbleType::Credits: return st::creditsBg3->b; + } + Unexpected("Type in Premium::BubbleWidget."); + }()); +} + +void AddBubbleRow( + not_null parent, + const style::PremiumBubble &st, + rpl::producer<> showFinishes, + int min, + int current, + int max, + BubbleType type, + std::optional> phrase, + const style::icon *icon) { + AddBubbleRow( + parent, + st, + std::move(showFinishes), + rpl::single(BubbleRowState{ + .counter = current, + .ratio = (current - min) / float64(max - min), + }), + type, + ProcessTextFactory(phrase), + icon, + st::boxRowPadding); +} + +void AddBubbleRow( + not_null parent, + const style::PremiumBubble &st, + rpl::producer<> showFinishes, + rpl::producer state, + BubbleType type, + Fn text, + const style::icon *icon, + const style::margins &outerPadding) { + const auto container = parent->add( + object_ptr(parent, 0)); + const auto bubble = Ui::CreateChild( + container, + st, + text ? std::move(text) : ProcessTextFactory(std::nullopt), + std::move(state), + type, + std::move(showFinishes), + icon, + outerPadding); + rpl::combine( + container->sizeValue(), + bubble->sizeValue() + ) | rpl::start_with_next([=](const QSize &parentSize, const QSize &size) { + container->resize(parentSize.width(), size.height()); + }, bubble->lifetime()); + bubble->show(); +} + +} // namespace Ui::Premium diff --git a/Telegram/SourceFiles/ui/effects/premium_bubble.h b/Telegram/SourceFiles/ui/effects/premium_bubble.h new file mode 100644 index 000000000..88e37771d --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/premium_bubble.h @@ -0,0 +1,168 @@ +/* +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 +*/ +#pragma once + +#include "ui/effects/numbers_animation.h" +#include "ui/rp_widget.h" + +enum lngtag_count : int; + +namespace tr { +template +struct phrase; +} // namespace tr + +namespace style { +struct PremiumBubble; +} // namespace style + +namespace Ui { +class VerticalLayout; +} // namespace Ui + +namespace Ui::Premium { + +using TextFactory = Fn; + +[[nodiscard]] TextFactory ProcessTextFactory( + std::optional> phrase); + +class Bubble final { +public: + using EdgeProgress = float64; + + Bubble( + const style::PremiumBubble &st, + Fn updateCallback, + TextFactory textFactory, + const style::icon *icon, + bool hasTail); + + [[nodiscard]] static crl::time SlideNoDeflectionDuration(); + + [[nodiscard]] int counter() const; + [[nodiscard]] int height() const; + [[nodiscard]] int width() const; + [[nodiscard]] int bubbleRadius() const; + [[nodiscard]] int countMaxWidth(int maxPossibleCounter) const; + + void setCounter(int value); + void setTailEdge(EdgeProgress edge); + void setFlipHorizontal(bool value); + void paintBubble(QPainter &p, const QRect &r, const QBrush &brush); + + [[nodiscard]] rpl::producer<> widthChanges() const; + +private: + [[nodiscard]] int filledWidth() const; + + const style::PremiumBubble &_st; + + const Fn _updateCallback; + const TextFactory _textFactory; + + const style::icon *_icon; + NumbersAnimation _numberAnimation; + const int _height; + const int _textTop; + const bool _hasTail; + + int _counter = -1; + EdgeProgress _tailEdge = 0.; + bool _flipHorizontal = false; + + rpl::event_stream<> _widthChanges; + +}; + +struct BubbleRowState { + int counter = 0; + float64 ratio = 0.; + bool animateFromZero = false; + bool dynamic = false; +}; + +enum class BubbleType : uchar { + NoPremium, + Premium, + Credits, +}; + +class BubbleWidget final : public Ui::RpWidget { +public: + BubbleWidget( + not_null parent, + const style::PremiumBubble &st, + TextFactory textFactory, + rpl::producer state, + BubbleType type, + rpl::producer<> showFinishes, + const style::icon *icon, + const style::margins &outerPadding); + +protected: + void paintEvent(QPaintEvent *e) override; + +private: + struct GradientParams { + int left = 0; + int width = 0; + int outer = 0; + + friend inline constexpr bool operator==( + GradientParams, + GradientParams) = default; + }; + void animateTo(BubbleRowState state); + + const style::PremiumBubble &_st; + BubbleRowState _animatingFrom; + float64 _animatingFromResultRatio = 0.; + float64 _animatingFromBubbleEdge = 0.; + rpl::variable _state; + Bubble _bubble; + int _maxBubbleWidth = 0; + const BubbleType _type; + const style::margins _outerPadding; + + Ui::Animations::Simple _appearanceAnimation; + QSize _spaceForDeflection; + + QLinearGradient _cachedGradient; + std::optional _cachedGradientParams; + + float64 _deflection; + + bool _ignoreDeflection = false; + float64 _stepBeforeDeflection; + float64 _stepAfterDeflection; + +}; + +void AddBubbleRow( + not_null parent, + const style::PremiumBubble &st, + rpl::producer<> showFinishes, + int min, + int current, + int max, + BubbleType type, + std::optional> phrase, + const style::icon *icon); + +void AddBubbleRow( + not_null parent, + const style::PremiumBubble &st, + rpl::producer<> showFinishes, + rpl::producer state, + BubbleType type, + Fn text, + const style::icon *icon, + const style::margins &outerPadding); + +} // namespace Ui::Premium diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp index 54361d545..1107e5f78 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/effects/animations.h" #include "ui/effects/gradient.h" #include "ui/effects/numbers_animation.h" +#include "ui/effects/premium_bubble.h" #include "ui/text/text_utilities.h" #include "ui/layers/generic_box.h" #include "ui/text/text_options.h" @@ -35,17 +36,6 @@ namespace Ui { namespace Premium { namespace { -using TextFactory = Fn; - -constexpr auto kBubbleRadiusSubtractor = 2; -constexpr auto kDeflectionSmall = 20.; -constexpr auto kDeflection = 30.; -constexpr auto kSlideDuration = crl::time(1000); - -constexpr auto kStepBeforeDeflection = 0.75; -constexpr auto kStepAfterDeflection = kStepBeforeDeflection - + (1. - kStepBeforeDeflection) / 2.; - class GradientRadioView : public Ui::RadioView { public: GradientRadioView( @@ -114,36 +104,6 @@ void GradientRadioView::setBrush(std::optional brush) { _brushOverride = brush; } -[[nodiscard]] TextFactory ProcessTextFactory( - std::optional> phrase) { - return phrase - ? TextFactory([=](int n) { return (*phrase)(tr::now, lt_count, n); }) - : TextFactory([=](int n) { return QString::number(n); }); -} - -[[nodiscard]] QLinearGradient ComputeGradient( - not_null content, - int left, - int width) { - - // Take a full width of parent box without paddings. - const auto fullGradientWidth = content->parentWidget()->width(); - auto fullGradient = QLinearGradient(0, 0, fullGradientWidth, 0); - fullGradient.setStops(ButtonGradientStops()); - - auto gradient = QLinearGradient(0, 0, width, 0); - const auto fullFinal = float64(fullGradient.finalStop().x()); - left += ((fullGradientWidth - content->width()) / 2); - gradient.setColorAt( - .0, - anim::gradient_color_at(fullGradient, left / fullFinal)); - gradient.setColorAt( - 1., - anim::gradient_color_at(fullGradient, (left + width) / fullFinal)); - - return gradient; -} - class PartialGradient final { public: PartialGradient(int from, int to, QGradientStops stops); @@ -183,465 +143,6 @@ QLinearGradient PartialGradient::compute(int position, int size) const { return resultGradient; } -class Bubble final { -public: - using EdgeProgress = float64; - - Bubble( - const style::PremiumBubble &st, - Fn updateCallback, - TextFactory textFactory, - const style::icon *icon, - bool premiumPossible); - - [[nodiscard]] int counter() const; - [[nodiscard]] int height() const; - [[nodiscard]] int width() const; - [[nodiscard]] int bubbleRadius() const; - [[nodiscard]] int countMaxWidth(int maxPossibleCounter) const; - - void setCounter(int value); - void setTailEdge(EdgeProgress edge); - void setFlipHorizontal(bool value); - void paintBubble(QPainter &p, const QRect &r, const QBrush &brush); - - [[nodiscard]] rpl::producer<> widthChanges() const; - -private: - [[nodiscard]] int filledWidth() const; - - const style::PremiumBubble &_st; - - const Fn _updateCallback; - const TextFactory _textFactory; - - const style::icon *_icon; - NumbersAnimation _numberAnimation; - const int _height; - const int _textTop; - const bool _premiumPossible; - - int _counter = -1; - EdgeProgress _tailEdge = 0.; - bool _flipHorizontal = false; - - rpl::event_stream<> _widthChanges; - -}; - -Bubble::Bubble( - const style::PremiumBubble &st, - Fn updateCallback, - TextFactory textFactory, - const style::icon *icon, - bool premiumPossible) -: _st(st) -, _updateCallback(std::move(updateCallback)) -, _textFactory(std::move(textFactory)) -, _icon(icon) -, _numberAnimation(_st.font, _updateCallback) -, _height(_st.height + _st.tailSize.height()) -, _textTop((_height - _st.tailSize.height() - _st.font->height) / 2) -, _premiumPossible(premiumPossible) { - _numberAnimation.setDisabledMonospace(true); - _numberAnimation.setWidthChangedCallback([=] { - _widthChanges.fire({}); - }); - _numberAnimation.setText(_textFactory(0), 0); - _numberAnimation.finishAnimating(); -} - -int Bubble::counter() const { - return _counter; -} - -int Bubble::height() const { - return _height; -} - -int Bubble::bubbleRadius() const { - return (_height - _st.tailSize.height()) / 2 - kBubbleRadiusSubtractor; -} - -int Bubble::filledWidth() const { - return _st.padding.left() - + _icon->width() - + _st.textSkip - + _st.padding.right(); -} - -int Bubble::width() const { - return filledWidth() + _numberAnimation.countWidth(); -} - -int Bubble::countMaxWidth(int maxPossibleCounter) const { - auto numbers = Ui::NumbersAnimation(_st.font, [] {}); - numbers.setDisabledMonospace(true); - numbers.setDuration(0); - numbers.setText(_textFactory(0), 0); - numbers.setText(_textFactory(maxPossibleCounter), maxPossibleCounter); - numbers.finishAnimating(); - return filledWidth() + numbers.maxWidth(); -} - -void Bubble::setCounter(int value) { - if (_counter != value) { - _counter = value; - _numberAnimation.setText(_textFactory(_counter), _counter); - } -} - -void Bubble::setTailEdge(EdgeProgress edge) { - _tailEdge = std::clamp(edge, 0., 1.); -} - -void Bubble::setFlipHorizontal(bool value) { - _flipHorizontal = value; -} - -void Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) { - if (_counter < 0) { - return; - } - - const auto penWidth = _st.penWidth; - const auto penWidthHalf = penWidth / 2; - const auto bubbleRect = r - style::margins( - penWidthHalf, - penWidthHalf, - penWidthHalf, - _st.tailSize.height() + penWidthHalf); - { - const auto radius = bubbleRadius(); - auto pathTail = QPainterPath(); - - const auto tailWHalf = _st.tailSize.width() / 2.; - const auto progress = _tailEdge; - - const auto tailTop = bubbleRect.y() + bubbleRect.height(); - const auto tailLeftFull = bubbleRect.x() - + (bubbleRect.width() * 0.5) - - tailWHalf; - const auto tailLeft = bubbleRect.x() - + (bubbleRect.width() * 0.5 * (progress + 1.)) - - tailWHalf; - const auto tailCenter = tailLeft + tailWHalf; - const auto tailRight = [&] { - const auto max = bubbleRect.x() + bubbleRect.width(); - const auto right = tailLeft + _st.tailSize.width(); - const auto bottomMax = max - radius; - return (right > bottomMax) - ? std::max(float64(tailCenter), float64(bottomMax)) - : right; - }(); - if (_premiumPossible) { - pathTail.moveTo(tailLeftFull, tailTop); - pathTail.lineTo(tailLeft, tailTop); - pathTail.lineTo(tailCenter, tailTop + _st.tailSize.height()); - pathTail.lineTo(tailRight, tailTop); - pathTail.lineTo(tailRight, tailTop - radius); - pathTail.moveTo(tailLeftFull, tailTop); - } - auto pathBubble = QPainterPath(); - pathBubble.setFillRule(Qt::WindingFill); - pathBubble.addRoundedRect(bubbleRect, radius, radius); - - auto hq = PainterHighQualityEnabler(p); - p.setPen(QPen( - brush, - penWidth, - Qt::SolidLine, - Qt::RoundCap, - Qt::RoundJoin)); - p.setBrush(brush); - if (_flipHorizontal) { - auto m = QTransform(); - const auto center = bubbleRect.center(); - m.translate(center.x(), center.y()); - m.scale(-1., 1.); - m.translate(-center.x(), -center.y()); - m.translate(-bubbleRect.left() + 1., 0); - p.drawPath(m.map(pathTail + pathBubble)); - } else { - p.drawPath(pathTail + pathBubble); - } - } - p.setPen(st::activeButtonFg); - p.setFont(_st.font); - const auto iconLeft = r.x() + _st.padding.left(); - _icon->paint( - p, - iconLeft, - bubbleRect.y() + (bubbleRect.height() - _icon->height()) / 2, - bubbleRect.width()); - _numberAnimation.paint( - p, - iconLeft + _icon->width() + _st.textSkip, - r.y() + _textTop, - width() / 2); -} - -rpl::producer<> Bubble::widthChanges() const { - return _widthChanges.events(); -} - -class BubbleWidget final : public Ui::RpWidget { -public: - BubbleWidget( - not_null parent, - const style::PremiumBubble &st, - TextFactory textFactory, - rpl::producer state, - bool premiumPossible, - rpl::producer<> showFinishes, - const style::icon *icon, - const style::margins &outerPadding); - -protected: - void paintEvent(QPaintEvent *e) override; - -private: - struct GradientParams { - int left = 0; - int width = 0; - int outer = 0; - - friend inline constexpr bool operator==( - GradientParams, - GradientParams) = default; - }; - void animateTo(BubbleRowState state); - - const style::PremiumBubble &_st; - BubbleRowState _animatingFrom; - float64 _animatingFromResultRatio = 0.; - rpl::variable _state; - Bubble _bubble; - int _maxBubbleWidth = 0; - const bool _premiumPossible; - const style::margins _outerPadding; - - Ui::Animations::Simple _appearanceAnimation; - QSize _spaceForDeflection; - - QLinearGradient _cachedGradient; - std::optional _cachedGradientParams; - - float64 _deflection; - - bool _ignoreDeflection = false; - float64 _stepBeforeDeflection; - float64 _stepAfterDeflection; - -}; - -BubbleWidget::BubbleWidget( - not_null parent, - const style::PremiumBubble &st, - TextFactory textFactory, - rpl::producer state, - bool premiumPossible, - rpl::producer<> showFinishes, - const style::icon *icon, - const style::margins &outerPadding) -: RpWidget(parent) -, _st(st) -, _state(std::move(state)) -, _bubble( - _st, - [=] { update(); }, - std::move(textFactory), - icon, - premiumPossible) -, _premiumPossible(premiumPossible) -, _outerPadding(outerPadding) -, _deflection(kDeflection) -, _stepBeforeDeflection(kStepBeforeDeflection) -, _stepAfterDeflection(kStepAfterDeflection) { - const auto resizeTo = [=](int w, int h) { - _deflection = (w > _st.widthLimit) - ? kDeflectionSmall - : kDeflection; - _spaceForDeflection = QSize(_st.skip, _st.skip); - resize(QSize(w, h) + _spaceForDeflection); - }; - - resizeTo(_bubble.width(), _bubble.height()); - _bubble.widthChanges( - ) | rpl::start_with_next([=] { - resizeTo(_bubble.width(), _bubble.height()); - }, lifetime()); - - std::move( - showFinishes - ) | rpl::take(1) | rpl::start_with_next([=] { - _state.value( - ) | rpl::start_with_next([=](BubbleRowState state) { - animateTo(state); - }, lifetime()); - }, lifetime()); -} - -void BubbleWidget::animateTo(BubbleRowState state) { - _maxBubbleWidth = _bubble.countMaxWidth(state.counter); - const auto parent = parentWidget(); - const auto computeLeft = [=](float64 pointRatio, float64 animProgress) { - const auto halfWidth = (_maxBubbleWidth / 2); - const auto left = _outerPadding.left(); - const auto right = _outerPadding.right(); - const auto available = parent->width() - left - right; - const auto delta = (pointRatio - _animatingFromResultRatio); - const auto center = available - * (_animatingFromResultRatio + delta * animProgress); - return center - halfWidth + left; - }; - const auto moveEndPoint = state.ratio; - const auto computeEdge = [=] { - return parent->width() - - _outerPadding.right() - - _maxBubbleWidth; - }; - struct LeftEdge final { - float64 goodPointRatio = 0.; - float64 bubbleLeftEdge = 0.; - }; - const auto leftEdge = [&]() -> LeftEdge { - const auto finish = computeLeft(moveEndPoint, 1.); - const auto &padding = _outerPadding; - if (finish <= padding.left()) { - const auto halfWidth = (_maxBubbleWidth / 2); - const auto goodPointRatio = float64(halfWidth) - / (parent->width() - padding.left() - padding.right()); - const auto bubbleLeftEdge = (padding.left() - finish) - / (_maxBubbleWidth / 2.); - return { goodPointRatio, bubbleLeftEdge }; - } - return {}; - }(); - const auto checkBubbleRightEdge = [&]() -> Bubble::EdgeProgress { - const auto finish = computeLeft(moveEndPoint, 1.); - const auto edge = computeEdge(); - return (finish >= edge) - ? (finish - edge) / (_maxBubbleWidth / 2.) - : 0.; - }; - const auto bubbleRightEdge = checkBubbleRightEdge(); - _ignoreDeflection = !_state.current().dynamic - && (bubbleRightEdge || leftEdge.goodPointRatio); - if (_ignoreDeflection) { - _stepBeforeDeflection = 1.; - _stepAfterDeflection = 1.; - } - const auto resultMoveEndPoint = leftEdge.goodPointRatio - ? leftEdge.goodPointRatio - : moveEndPoint; - _bubble.setFlipHorizontal(leftEdge.bubbleLeftEdge); - - const auto duration = kSlideDuration - * (_ignoreDeflection ? kStepBeforeDeflection : 1.) - * ((_state.current().ratio < 0.001) ? 0.5 : 1.); - if (state.animateFromZero) { - _animatingFrom.ratio = 0.; - _animatingFrom.counter = 0; - _animatingFromResultRatio = 0.; - } - _appearanceAnimation.start([=](float64 value) { - if (!_appearanceAnimation.animating()) { - _animatingFrom = state; - _animatingFromResultRatio = resultMoveEndPoint; - } - const auto moveProgress = std::clamp( - (value / _stepBeforeDeflection), - 0., - 1.); - const auto counterProgress = std::clamp( - (value / _stepAfterDeflection), - 0., - 1.); - moveToLeft( - std::max( - int(base::SafeRound( - (computeLeft(resultMoveEndPoint, moveProgress) - - (_maxBubbleWidth / 2.) * bubbleRightEdge))), - 0), - 0); - - const auto now = _animatingFrom.counter - + counterProgress * (state.counter - _animatingFrom.counter); - _bubble.setCounter(int(base::SafeRound(now))); - - const auto edgeProgress = leftEdge.bubbleLeftEdge - ? leftEdge.bubbleLeftEdge - : (bubbleRightEdge * value); - _bubble.setTailEdge(edgeProgress); - update(); - }, - 0., - 1., - duration, - anim::easeOutCirc); -} - -void BubbleWidget::paintEvent(QPaintEvent *e) { - if (_bubble.counter() < 0) { - return; - } - - auto p = QPainter(this); - - const auto padding = QMargins( - 0, - _spaceForDeflection.height(), - _spaceForDeflection.width(), - 0); - const auto bubbleRect = rect() - padding; - - const auto params = GradientParams{ - .left = x(), - .width = bubbleRect.width(), - .outer = parentWidget()->parentWidget()->width(), - }; - if (_cachedGradientParams != params) { - _cachedGradient = ComputeGradient( - parentWidget(), - params.left, - params.width); - _cachedGradientParams = params; - } - if (_appearanceAnimation.animating()) { - const auto progress = _appearanceAnimation.value(1.); - const auto finalScale = (_animatingFromResultRatio > 0.) - || (_state.current().ratio < 0.001); - const auto scaleProgress = finalScale - ? 1. - : std::clamp((progress / _stepBeforeDeflection), 0., 1.); - const auto scale = scaleProgress; - const auto rotationProgress = std::clamp( - (progress - _stepBeforeDeflection) / (1. - _stepBeforeDeflection), - 0., - 1.); - const auto rotationProgressReverse = std::clamp( - (progress - _stepAfterDeflection) / (1. - _stepAfterDeflection), - 0., - 1.); - - const auto offsetX = bubbleRect.x() + bubbleRect.width() / 2; - const auto offsetY = bubbleRect.y() + bubbleRect.height(); - p.translate(offsetX, offsetY); - p.scale(scale, scale); - if (!_ignoreDeflection) { - p.rotate(rotationProgress * _deflection - - rotationProgressReverse * _deflection); - } - p.translate(-offsetX, -offsetY); - } - - _bubble.paintBubble( - p, - bubbleRect, - _premiumPossible ? QBrush(_cachedGradient) : st::windowBgActive->b); -} - class Line final : public Ui::RpWidget { public: Line( @@ -747,7 +248,7 @@ Line::Line( const auto from = state.animateFromZero ? 0. : _animation.value(_ratio); - const auto duration = kSlideDuration * kStepBeforeDeflection; + const auto duration = Bubble::SlideNoDeflectionDuration(); _animation.start([=] { update(); }, from, state.ratio, duration, anim::easeOutCirc); @@ -969,59 +470,6 @@ QImage GenerateStarForLightTopBar(QRectF rect) { return frame; } -void AddBubbleRow( - not_null parent, - const style::PremiumBubble &st, - rpl::producer<> showFinishes, - int min, - int current, - int max, - bool premiumPossible, - std::optional> phrase, - const style::icon *icon) { - AddBubbleRow( - parent, - st, - std::move(showFinishes), - rpl::single(BubbleRowState{ - .counter = current, - .ratio = (current - min) / float64(max - min), - }), - premiumPossible, - ProcessTextFactory(phrase), - icon, - st::boxRowPadding); -} - -void AddBubbleRow( - not_null parent, - const style::PremiumBubble &st, - rpl::producer<> showFinishes, - rpl::producer state, - bool premiumPossible, - Fn text, - const style::icon *icon, - const style::margins &outerPadding) { - const auto container = parent->add( - object_ptr(parent, 0)); - const auto bubble = Ui::CreateChild( - container, - st, - text ? std::move(text) : ProcessTextFactory(std::nullopt), - std::move(state), - premiumPossible, - std::move(showFinishes), - icon, - outerPadding); - rpl::combine( - container->sizeValue(), - bubble->sizeValue() - ) | rpl::start_with_next([=](const QSize &parentSize, const QSize &size) { - container->resize(parentSize.width(), size.height()); - }, bubble->lifetime()); - bubble->show(); -} - void AddLimitRow( not_null parent, const style::PremiumLimits &st, @@ -1250,6 +698,28 @@ QGradientStops CreditsIconGradientStops() { }; } +QLinearGradient ComputeGradient( + not_null content, + int left, + int width) { + // Take a full width of parent box without paddings. + const auto fullGradientWidth = content->parentWidget()->width(); + auto fullGradient = QLinearGradient(0, 0, fullGradientWidth, 0); + fullGradient.setStops(ButtonGradientStops()); + + auto gradient = QLinearGradient(0, 0, width, 0); + const auto fullFinal = float64(fullGradient.finalStop().x()); + left += ((fullGradientWidth - content->width()) / 2); + gradient.setColorAt( + .0, + anim::gradient_color_at(fullGradient, left / fullFinal)); + gradient.setColorAt( + 1., + anim::gradient_color_at(fullGradient, (left + width) / fullFinal)); + + return gradient; +} + void ShowListBox( not_null box, const style::PremiumLimits &st, diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.h b/Telegram/SourceFiles/ui/effects/premium_graphics.h index d1ba4f9d7..10fe14ac8 100644 --- a/Telegram/SourceFiles/ui/effects/premium_graphics.h +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.h @@ -45,33 +45,6 @@ inline constexpr auto kLimitRowRatio = 0.5; [[nodiscard]] QByteArray ColorizedSvg(const QGradientStops &gradientStops); [[nodiscard]] QImage GenerateStarForLightTopBar(QRectF rect); -void AddBubbleRow( - not_null parent, - const style::PremiumBubble &st, - rpl::producer<> showFinishes, - int min, - int current, - int max, - bool premiumPossible, - std::optional> phrase, - const style::icon *icon); - -struct BubbleRowState { - int counter = 0; - float64 ratio = 0.; - bool animateFromZero = false; - bool dynamic = false; -}; -void AddBubbleRow( - not_null parent, - const style::PremiumBubble &st, - rpl::producer<> showFinishes, - rpl::producer state, - bool premiumPossible, - Fn text, - const style::icon *icon, - const style::margins &outerPadding); - void AddLimitRow( not_null parent, const style::PremiumLimits &st, @@ -130,6 +103,11 @@ void AddAccountsRow( [[nodiscard]] QGradientStops GiftGradientStops(); [[nodiscard]] QGradientStops CreditsIconGradientStops(); +[[nodiscard]] QLinearGradient ComputeGradient( + not_null content, + int left, + int width); + struct ListEntry final { rpl::producer title; rpl::producer about; diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 1bb4eba5d..eecda4a73 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -378,6 +378,8 @@ PRIVATE ui/effects/loading_element.h ui/effects/outline_segments.cpp ui/effects/outline_segments.h + ui/effects/premium_bubble.cpp + ui/effects/premium_bubble.h ui/effects/premium_graphics.cpp ui/effects/premium_graphics.h ui/effects/premium_stars.cpp diff --git a/Telegram/lib_ui b/Telegram/lib_ui index a5b1266a8..95229cd46 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit a5b1266a8c340ed916466a83cbbc5793471c2438 +Subproject commit 95229cd46bbba42b431a097705494ec39cce5f0c