mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-18 15:17:07 +02:00
Improve paid reactions box design.
This commit is contained in:
parent
273e041935
commit
02610de010
13 changed files with 875 additions and 641 deletions
Telegram
|
@ -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}.";
|
||||
|
|
|
@ -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<QString> 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);
|
||||
|
|
|
@ -34,7 +34,7 @@ namespace Payments {
|
|||
namespace {
|
||||
|
||||
constexpr auto kMaxPerReactionFallback = 2'500;
|
||||
constexpr auto kDefaultPerReaction = 20;
|
||||
constexpr auto kDefaultPerReaction = 50;
|
||||
|
||||
void TryAddingPaidReaction(
|
||||
not_null<Main::Session*> session,
|
||||
|
@ -82,6 +82,17 @@ void TryAddingPaidReaction(
|
|||
done);
|
||||
}
|
||||
|
||||
[[nodiscard]] int CountLocalPaid(not_null<HistoryItem*> item) {
|
||||
const auto paid = [](const std::vector<Data::MessageReaction> &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<int>(
|
||||
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<Ui::BoxContent> selectBox;
|
||||
|
@ -169,10 +179,14 @@ void ShowPaidReactionDetails(
|
|||
};
|
||||
});
|
||||
};
|
||||
auto already = 0;
|
||||
auto top = std::vector<Ui::PaidReactionTop>();
|
||||
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),
|
||||
|
|
|
@ -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<int(float64)> ratioToValue;
|
||||
Fn<float64(int)> 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<float64, int>();
|
||||
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<VerticalLayout*> container,
|
||||
int min,
|
||||
int current,
|
||||
int max,
|
||||
Fn<void(int)> changed) {
|
||||
const auto top = st::boxTitleClose.height + st::creditsHistoryRightSkip;
|
||||
Expects(current >= 1 && current <= max);
|
||||
|
||||
const auto slider = container->add(
|
||||
object_ptr<MediaSlider>(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<Ui::RpWidget*> MakeTopReactor(
|
||||
[[nodiscard]] not_null<RpWidget*> MakeTopReactor(
|
||||
not_null<QWidget*> parent,
|
||||
const PaidReactionTop &data) {
|
||||
const auto result = Ui::CreateChild<Ui::RpWidget>(parent);
|
||||
const auto result = CreateChild<RpWidget>(parent);
|
||||
result->show();
|
||||
|
||||
struct State {
|
||||
QImage badge;
|
||||
Ui::Text::String name;
|
||||
Text::String name;
|
||||
};
|
||||
const auto state = result->lifetime().make_state<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<Ui::FixedHeightWidget>(container, height),
|
||||
object_ptr<FixedHeightWidget>(container, height),
|
||||
st::paidReactTopMargin);
|
||||
struct State {
|
||||
std::vector<not_null<Ui::RpWidget*>> widgets;
|
||||
std::vector<not_null<RpWidget*>> widgets;
|
||||
};
|
||||
const auto state = wrap->lifetime().make_state<State>();
|
||||
|
||||
|
@ -189,6 +262,9 @@ void FillTopReactors(
|
|||
void PaidReactionsBox(
|
||||
not_null<GenericBox*> 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<Ui::FlatLabel>(
|
||||
object_ptr<FlatLabel>(
|
||||
box,
|
||||
tr::lng_paid_react_title(),
|
||||
st::boostCenteredTitle),
|
||||
st::boxRowPadding + QMargins(0, st::paidReactTitleSkip, 0, 0));
|
||||
box->addRow(
|
||||
object_ptr<Ui::FlatLabel>(
|
||||
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<RpWidget>(box),
|
||||
(st::boxRowPadding
|
||||
+ QMargins(0, st::lineWidth, 0, st::boostBottomSkip)));
|
||||
const auto label = CreateChild<FlatLabel>(
|
||||
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<Ui::FlatLabel>(
|
||||
const auto buttonLabel = CreateChild<FlatLabel>(
|
||||
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(
|
||||
|
|
|
@ -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<PaidReactionTop> top;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
475
Telegram/SourceFiles/ui/effects/premium_bubble.cpp
Normal file
475
Telegram/SourceFiles/ui/effects/premium_bubble.cpp
Normal file
|
@ -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<tr::phrase<lngtag_count>> 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<void()> 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<Ui::RpWidget*> parent,
|
||||
const style::PremiumBubble &st,
|
||||
TextFactory textFactory,
|
||||
rpl::producer<BubbleRowState> 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<Ui::VerticalLayout*> parent,
|
||||
const style::PremiumBubble &st,
|
||||
rpl::producer<> showFinishes,
|
||||
int min,
|
||||
int current,
|
||||
int max,
|
||||
BubbleType type,
|
||||
std::optional<tr::phrase<lngtag_count>> 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<Ui::VerticalLayout*> parent,
|
||||
const style::PremiumBubble &st,
|
||||
rpl::producer<> showFinishes,
|
||||
rpl::producer<BubbleRowState> state,
|
||||
BubbleType type,
|
||||
Fn<QString(int)> text,
|
||||
const style::icon *icon,
|
||||
const style::margins &outerPadding) {
|
||||
const auto container = parent->add(
|
||||
object_ptr<Ui::FixedHeightWidget>(parent, 0));
|
||||
const auto bubble = Ui::CreateChild<BubbleWidget>(
|
||||
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
|
168
Telegram/SourceFiles/ui/effects/premium_bubble.h
Normal file
168
Telegram/SourceFiles/ui/effects/premium_bubble.h
Normal file
|
@ -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 <typename ...Tags>
|
||||
struct phrase;
|
||||
} // namespace tr
|
||||
|
||||
namespace style {
|
||||
struct PremiumBubble;
|
||||
} // namespace style
|
||||
|
||||
namespace Ui {
|
||||
class VerticalLayout;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Ui::Premium {
|
||||
|
||||
using TextFactory = Fn<QString(int)>;
|
||||
|
||||
[[nodiscard]] TextFactory ProcessTextFactory(
|
||||
std::optional<tr::phrase<lngtag_count>> phrase);
|
||||
|
||||
class Bubble final {
|
||||
public:
|
||||
using EdgeProgress = float64;
|
||||
|
||||
Bubble(
|
||||
const style::PremiumBubble &st,
|
||||
Fn<void()> 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<void()> _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<Ui::RpWidget*> parent,
|
||||
const style::PremiumBubble &st,
|
||||
TextFactory textFactory,
|
||||
rpl::producer<BubbleRowState> 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<BubbleRowState> _state;
|
||||
Bubble _bubble;
|
||||
int _maxBubbleWidth = 0;
|
||||
const BubbleType _type;
|
||||
const style::margins _outerPadding;
|
||||
|
||||
Ui::Animations::Simple _appearanceAnimation;
|
||||
QSize _spaceForDeflection;
|
||||
|
||||
QLinearGradient _cachedGradient;
|
||||
std::optional<GradientParams> _cachedGradientParams;
|
||||
|
||||
float64 _deflection;
|
||||
|
||||
bool _ignoreDeflection = false;
|
||||
float64 _stepBeforeDeflection;
|
||||
float64 _stepAfterDeflection;
|
||||
|
||||
};
|
||||
|
||||
void AddBubbleRow(
|
||||
not_null<Ui::VerticalLayout*> parent,
|
||||
const style::PremiumBubble &st,
|
||||
rpl::producer<> showFinishes,
|
||||
int min,
|
||||
int current,
|
||||
int max,
|
||||
BubbleType type,
|
||||
std::optional<tr::phrase<lngtag_count>> phrase,
|
||||
const style::icon *icon);
|
||||
|
||||
void AddBubbleRow(
|
||||
not_null<Ui::VerticalLayout*> parent,
|
||||
const style::PremiumBubble &st,
|
||||
rpl::producer<> showFinishes,
|
||||
rpl::producer<BubbleRowState> state,
|
||||
BubbleType type,
|
||||
Fn<QString(int)> text,
|
||||
const style::icon *icon,
|
||||
const style::margins &outerPadding);
|
||||
|
||||
} // namespace Ui::Premium
|
|
@ -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<QString(int)>;
|
||||
|
||||
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<QBrush> brush) {
|
|||
_brushOverride = brush;
|
||||
}
|
||||
|
||||
[[nodiscard]] TextFactory ProcessTextFactory(
|
||||
std::optional<tr::phrase<lngtag_count>> 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<QWidget*> 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<void()> 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<void()> _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<void()> 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<Ui::RpWidget*> parent,
|
||||
const style::PremiumBubble &st,
|
||||
TextFactory textFactory,
|
||||
rpl::producer<BubbleRowState> 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<BubbleRowState> _state;
|
||||
Bubble _bubble;
|
||||
int _maxBubbleWidth = 0;
|
||||
const bool _premiumPossible;
|
||||
const style::margins _outerPadding;
|
||||
|
||||
Ui::Animations::Simple _appearanceAnimation;
|
||||
QSize _spaceForDeflection;
|
||||
|
||||
QLinearGradient _cachedGradient;
|
||||
std::optional<GradientParams> _cachedGradientParams;
|
||||
|
||||
float64 _deflection;
|
||||
|
||||
bool _ignoreDeflection = false;
|
||||
float64 _stepBeforeDeflection;
|
||||
float64 _stepAfterDeflection;
|
||||
|
||||
};
|
||||
|
||||
BubbleWidget::BubbleWidget(
|
||||
not_null<Ui::RpWidget*> parent,
|
||||
const style::PremiumBubble &st,
|
||||
TextFactory textFactory,
|
||||
rpl::producer<BubbleRowState> 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<Ui::VerticalLayout*> parent,
|
||||
const style::PremiumBubble &st,
|
||||
rpl::producer<> showFinishes,
|
||||
int min,
|
||||
int current,
|
||||
int max,
|
||||
bool premiumPossible,
|
||||
std::optional<tr::phrase<lngtag_count>> 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<Ui::VerticalLayout*> parent,
|
||||
const style::PremiumBubble &st,
|
||||
rpl::producer<> showFinishes,
|
||||
rpl::producer<BubbleRowState> state,
|
||||
bool premiumPossible,
|
||||
Fn<QString(int)> text,
|
||||
const style::icon *icon,
|
||||
const style::margins &outerPadding) {
|
||||
const auto container = parent->add(
|
||||
object_ptr<Ui::FixedHeightWidget>(parent, 0));
|
||||
const auto bubble = Ui::CreateChild<BubbleWidget>(
|
||||
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<Ui::VerticalLayout*> parent,
|
||||
const style::PremiumLimits &st,
|
||||
|
@ -1250,6 +698,28 @@ QGradientStops CreditsIconGradientStops() {
|
|||
};
|
||||
}
|
||||
|
||||
QLinearGradient ComputeGradient(
|
||||
not_null<QWidget*> 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<Ui::GenericBox*> box,
|
||||
const style::PremiumLimits &st,
|
||||
|
|
|
@ -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<Ui::VerticalLayout*> parent,
|
||||
const style::PremiumBubble &st,
|
||||
rpl::producer<> showFinishes,
|
||||
int min,
|
||||
int current,
|
||||
int max,
|
||||
bool premiumPossible,
|
||||
std::optional<tr::phrase<lngtag_count>> phrase,
|
||||
const style::icon *icon);
|
||||
|
||||
struct BubbleRowState {
|
||||
int counter = 0;
|
||||
float64 ratio = 0.;
|
||||
bool animateFromZero = false;
|
||||
bool dynamic = false;
|
||||
};
|
||||
void AddBubbleRow(
|
||||
not_null<Ui::VerticalLayout*> parent,
|
||||
const style::PremiumBubble &st,
|
||||
rpl::producer<> showFinishes,
|
||||
rpl::producer<BubbleRowState> state,
|
||||
bool premiumPossible,
|
||||
Fn<QString(int)> text,
|
||||
const style::icon *icon,
|
||||
const style::margins &outerPadding);
|
||||
|
||||
void AddLimitRow(
|
||||
not_null<Ui::VerticalLayout*> parent,
|
||||
const style::PremiumLimits &st,
|
||||
|
@ -130,6 +103,11 @@ void AddAccountsRow(
|
|||
[[nodiscard]] QGradientStops GiftGradientStops();
|
||||
[[nodiscard]] QGradientStops CreditsIconGradientStops();
|
||||
|
||||
[[nodiscard]] QLinearGradient ComputeGradient(
|
||||
not_null<QWidget*> content,
|
||||
int left,
|
||||
int width);
|
||||
|
||||
struct ListEntry final {
|
||||
rpl::producer<QString> title;
|
||||
rpl::producer<TextWithEntities> about;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit a5b1266a8c340ed916466a83cbbc5793471c2438
|
||||
Subproject commit 95229cd46bbba42b431a097705494ec39cce5f0c
|
Loading…
Add table
Reference in a new issue