mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-06-05 06:33:57 +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
|
@ -3419,7 +3419,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
"lng_paid_price" = "Unlock for {price}";
|
"lng_paid_price" = "Unlock for {price}";
|
||||||
|
|
||||||
"lng_paid_react_title" = "Star Reaction";
|
"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_top_title" = "Top Senders";
|
||||||
"lng_paid_react_send" = "Send {price}";
|
"lng_paid_react_send" = "Send {price}";
|
||||||
"lng_paid_react_agree" = "By sending stars, you agree to the {link}.";
|
"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/boxes/confirm_box.h"
|
||||||
#include "ui/controls/peer_list_dummy.h"
|
#include "ui/controls/peer_list_dummy.h"
|
||||||
|
#include "ui/effects/premium_bubble.h"
|
||||||
#include "ui/effects/premium_graphics.h"
|
#include "ui/effects/premium_graphics.h"
|
||||||
#include "ui/widgets/checkbox.h"
|
#include "ui/widgets/checkbox.h"
|
||||||
#include "ui/wrap/padding_wrap.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) {
|
void InactiveDelegate::peerListSetTitle(rpl::producer<QString> title) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,7 +428,7 @@ void SimpleLimitBox(
|
||||||
(descriptor.complexRatio
|
(descriptor.complexRatio
|
||||||
? descriptor.premiumLimit
|
? descriptor.premiumLimit
|
||||||
: 2 * descriptor.current),
|
: 2 * descriptor.current),
|
||||||
premiumPossible,
|
ChooseBubbleType(premiumPossible),
|
||||||
descriptor.phrase,
|
descriptor.phrase,
|
||||||
descriptor.icon);
|
descriptor.icon);
|
||||||
Ui::AddSkip(top, st::premiumLineTextSkip);
|
Ui::AddSkip(top, st::premiumLineTextSkip);
|
||||||
|
@ -1109,7 +1116,7 @@ void AccountsLimitBox(
|
||||||
: (current > defaultLimit)
|
: (current > defaultLimit)
|
||||||
? (current + 1)
|
? (current + 1)
|
||||||
: (defaultLimit * 2)),
|
: (defaultLimit * 2)),
|
||||||
premiumPossible,
|
ChooseBubbleType(premiumPossible),
|
||||||
std::nullopt,
|
std::nullopt,
|
||||||
&st::premiumIconAccounts);
|
&st::premiumIconAccounts);
|
||||||
Ui::AddSkip(top, st::premiumLineTextSkip);
|
Ui::AddSkip(top, st::premiumLineTextSkip);
|
||||||
|
|
|
@ -34,7 +34,7 @@ namespace Payments {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kMaxPerReactionFallback = 2'500;
|
constexpr auto kMaxPerReactionFallback = 2'500;
|
||||||
constexpr auto kDefaultPerReaction = 20;
|
constexpr auto kDefaultPerReaction = 50;
|
||||||
|
|
||||||
void TryAddingPaidReaction(
|
void TryAddingPaidReaction(
|
||||||
not_null<Main::Session*> session,
|
not_null<Main::Session*> session,
|
||||||
|
@ -82,6 +82,17 @@ void TryAddingPaidReaction(
|
||||||
done);
|
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
|
} // namespace
|
||||||
|
|
||||||
void TryAddingPaidReaction(
|
void TryAddingPaidReaction(
|
||||||
|
@ -110,13 +121,12 @@ void ShowPaidReactionDetails(
|
||||||
const auto session = &item->history()->session();
|
const auto session = &item->history()->session();
|
||||||
const auto appConfig = &session->appConfig();
|
const auto appConfig = &session->appConfig();
|
||||||
|
|
||||||
const auto min = 1;
|
|
||||||
const auto max = std::max(
|
const auto max = std::max(
|
||||||
appConfig->get<int>(
|
appConfig->get<int>(
|
||||||
u"stars_paid_reaction_amount_max"_q,
|
u"stars_paid_reaction_amount_max"_q,
|
||||||
kMaxPerReactionFallback),
|
kMaxPerReactionFallback),
|
||||||
min);
|
2);
|
||||||
const auto chosen = std::clamp(kDefaultPerReaction, min, max);
|
const auto chosen = std::clamp(kDefaultPerReaction, 1, max);
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
QPointer<Ui::BoxContent> selectBox;
|
QPointer<Ui::BoxContent> selectBox;
|
||||||
|
@ -169,10 +179,14 @@ void ShowPaidReactionDetails(
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
auto already = 0;
|
||||||
auto top = std::vector<Ui::PaidReactionTop>();
|
auto top = std::vector<Ui::PaidReactionTop>();
|
||||||
const auto &topPaid = item->topPaidReactions();
|
const auto &topPaid = item->topPaidReactions();
|
||||||
top.reserve(topPaid.size());
|
top.reserve(topPaid.size());
|
||||||
for (const auto &entry : topPaid) {
|
for (const auto &entry : topPaid) {
|
||||||
|
if (entry.my) {
|
||||||
|
already = entry.count;
|
||||||
|
}
|
||||||
if (!entry.top) {
|
if (!entry.top) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -185,9 +199,9 @@ void ShowPaidReactionDetails(
|
||||||
ranges::sort(top, ranges::greater(), &Ui::PaidReactionTop::count);
|
ranges::sort(top, ranges::greater(), &Ui::PaidReactionTop::count);
|
||||||
|
|
||||||
state->selectBox = show->show(Ui::MakePaidReactionBox({
|
state->selectBox = show->show(Ui::MakePaidReactionBox({
|
||||||
.min = min,
|
.already = already + CountLocalPaid(item),
|
||||||
.max = max,
|
|
||||||
.chosen = chosen,
|
.chosen = chosen,
|
||||||
|
.max = max,
|
||||||
.top = std::move(top),
|
.top = std::move(top),
|
||||||
.channel = item->history()->peer->name(),
|
.channel = item->history()->peer->name(),
|
||||||
.submit = std::move(submitText),
|
.submit = std::move(submitText),
|
||||||
|
|
|
@ -9,12 +9,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
|
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
#include "ui/boxes/boost_box.h" // MakeBoostFeaturesBadge.
|
#include "ui/boxes/boost_box.h" // MakeBoostFeaturesBadge.
|
||||||
|
#include "ui/effects/premium_bubble.h"
|
||||||
#include "ui/layers/generic_box.h"
|
#include "ui/layers/generic_box.h"
|
||||||
#include "ui/text/text_utilities.h"
|
#include "ui/text/text_utilities.h"
|
||||||
#include "ui/widgets/buttons.h"
|
#include "ui/widgets/buttons.h"
|
||||||
#include "ui/widgets/continuous_sliders.h"
|
#include "ui/widgets/continuous_sliders.h"
|
||||||
#include "ui/dynamic_image.h"
|
#include "ui/dynamic_image.h"
|
||||||
#include "ui/painter.h"
|
#include "ui/painter.h"
|
||||||
|
#include "ui/vertical_list.h"
|
||||||
#include "styles/style_chat.h"
|
#include "styles/style_chat.h"
|
||||||
#include "styles/style_credits.h"
|
#include "styles/style_credits.h"
|
||||||
#include "styles/style_layers.h"
|
#include "styles/style_layers.h"
|
||||||
|
@ -33,23 +35,94 @@ namespace {
|
||||||
|
|
||||||
constexpr auto kMaxTopPaidShown = 3;
|
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(
|
void PaidReactionSlider(
|
||||||
not_null<VerticalLayout*> container,
|
not_null<VerticalLayout*> container,
|
||||||
int min,
|
|
||||||
int current,
|
int current,
|
||||||
int max,
|
int max,
|
||||||
Fn<void(int)> changed) {
|
Fn<void(int)> changed) {
|
||||||
const auto top = st::boxTitleClose.height + st::creditsHistoryRightSkip;
|
Expects(current >= 1 && current <= max);
|
||||||
|
|
||||||
const auto slider = container->add(
|
const auto slider = container->add(
|
||||||
object_ptr<MediaSlider>(container, st::paidReactSlider),
|
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->resize(slider->width(), st::paidReactSlider.seekSize.height());
|
||||||
slider->setPseudoDiscrete(
|
|
||||||
max + 1 - min,
|
const auto discreter = DiscreterForMax(max);
|
||||||
[=](int index) { return min + index; },
|
slider->setAlwaysDisplayMarker(true);
|
||||||
current - min,
|
slider->setDirection(ContinuousSlider::Direction::Horizontal);
|
||||||
changed,
|
slider->setValue(discreter.valueToRatio(current));
|
||||||
changed);
|
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) {
|
[[nodiscard]] QImage GenerateBadgeImage(int count) {
|
||||||
|
@ -100,15 +173,15 @@ void PaidReactionSlider(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] not_null<Ui::RpWidget*> MakeTopReactor(
|
[[nodiscard]] not_null<RpWidget*> MakeTopReactor(
|
||||||
not_null<QWidget*> parent,
|
not_null<QWidget*> parent,
|
||||||
const PaidReactionTop &data) {
|
const PaidReactionTop &data) {
|
||||||
const auto result = Ui::CreateChild<Ui::RpWidget>(parent);
|
const auto result = CreateChild<RpWidget>(parent);
|
||||||
result->show();
|
result->show();
|
||||||
|
|
||||||
struct State {
|
struct State {
|
||||||
QImage badge;
|
QImage badge;
|
||||||
Ui::Text::String name;
|
Text::String name;
|
||||||
};
|
};
|
||||||
const auto state = result->lifetime().make_state<State>();
|
const auto state = result->lifetime().make_state<State>();
|
||||||
state->name.setText(st::defaultTextStyle, data.name);
|
state->name.setText(st::defaultTextStyle, data.name);
|
||||||
|
@ -159,10 +232,10 @@ void FillTopReactors(
|
||||||
|
|
||||||
const auto height = st::paidReactTopNameSkip + st::normalFont->height;
|
const auto height = st::paidReactTopNameSkip + st::normalFont->height;
|
||||||
const auto wrap = container->add(
|
const auto wrap = container->add(
|
||||||
object_ptr<Ui::FixedHeightWidget>(container, height),
|
object_ptr<FixedHeightWidget>(container, height),
|
||||||
st::paidReactTopMargin);
|
st::paidReactTopMargin);
|
||||||
struct State {
|
struct State {
|
||||||
std::vector<not_null<Ui::RpWidget*>> widgets;
|
std::vector<not_null<RpWidget*>> widgets;
|
||||||
};
|
};
|
||||||
const auto state = wrap->lifetime().make_state<State>();
|
const auto state = wrap->lifetime().make_state<State>();
|
||||||
|
|
||||||
|
@ -189,6 +262,9 @@ void FillTopReactors(
|
||||||
void PaidReactionsBox(
|
void PaidReactionsBox(
|
||||||
not_null<GenericBox*> box,
|
not_null<GenericBox*> box,
|
||||||
PaidReactionBoxArgs &&args) {
|
PaidReactionBoxArgs &&args) {
|
||||||
|
args.max = std::max(args.max, 2);
|
||||||
|
args.chosen = std::clamp(args.chosen, 1, args.max);
|
||||||
|
|
||||||
box->setWidth(st::boxWideWidth);
|
box->setWidth(st::boxWideWidth);
|
||||||
box->setStyle(st::paidReactBox);
|
box->setStyle(st::paidReactBox);
|
||||||
box->setNoContentMargin(true);
|
box->setNoContentMargin(true);
|
||||||
|
@ -201,41 +277,79 @@ void PaidReactionsBox(
|
||||||
const auto changed = [=](int count) {
|
const auto changed = [=](int count) {
|
||||||
state->chosen = count;
|
state->chosen = count;
|
||||||
};
|
};
|
||||||
PaidReactionSlider(
|
|
||||||
box->verticalLayout(),
|
const auto content = box->verticalLayout();
|
||||||
args.min,
|
AddSkip(content, st::boxTitleClose.height + st::paidReactBubbleTop);
|
||||||
args.chosen,
|
|
||||||
args.max,
|
const auto valueToRatio = DiscreterForMax(args.max).valueToRatio;
|
||||||
changed);
|
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->addTopButton(st::boxTitleClose, [=] { box->closeBox(); });
|
||||||
|
|
||||||
box->addRow(
|
box->addRow(
|
||||||
object_ptr<Ui::FlatLabel>(
|
object_ptr<FlatLabel>(
|
||||||
box,
|
box,
|
||||||
tr::lng_paid_react_title(),
|
tr::lng_paid_react_title(),
|
||||||
st::boostCenteredTitle),
|
st::boostCenteredTitle),
|
||||||
st::boxRowPadding + QMargins(0, st::paidReactTitleSkip, 0, 0));
|
st::boxRowPadding + QMargins(0, st::paidReactTitleSkip, 0, 0));
|
||||||
box->addRow(
|
const auto labelWrap = box->addRow(
|
||||||
object_ptr<Ui::FlatLabel>(
|
object_ptr<RpWidget>(box),
|
||||||
box,
|
|
||||||
tr::lng_paid_react_about(
|
|
||||||
lt_channel,
|
|
||||||
rpl::single(Text::Bold(args.channel)),
|
|
||||||
Text::RichLangValue),
|
|
||||||
st::boostText),
|
|
||||||
(st::boxRowPadding
|
(st::boxRowPadding
|
||||||
+ QMargins(0, st::lineWidth, 0, st::boostBottomSkip)));
|
+ 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()) {
|
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()), [=] {
|
const auto button = box->addButton(rpl::single(QString()), [=] {
|
||||||
args.send(state->chosen.current());
|
args.send(state->chosen.current());
|
||||||
});
|
});
|
||||||
{
|
{
|
||||||
const auto buttonLabel = Ui::CreateChild<Ui::FlatLabel>(
|
const auto buttonLabel = CreateChild<FlatLabel>(
|
||||||
button,
|
button,
|
||||||
rpl::single(QString()),
|
rpl::single(QString()),
|
||||||
st::creditsBoxButtonLabel);
|
st::creditsBoxButtonLabel);
|
||||||
|
@ -268,7 +382,7 @@ void PaidReactionsBox(
|
||||||
|
|
||||||
{
|
{
|
||||||
const auto balance = Settings::AddBalanceWidget(
|
const auto balance = Settings::AddBalanceWidget(
|
||||||
box->verticalLayout(),
|
content,
|
||||||
std::move(args.balanceValue),
|
std::move(args.balanceValue),
|
||||||
false);
|
false);
|
||||||
rpl::combine(
|
rpl::combine(
|
||||||
|
|
|
@ -27,9 +27,9 @@ struct PaidReactionTop {
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PaidReactionBoxArgs {
|
struct PaidReactionBoxArgs {
|
||||||
int min = 0;
|
int already = 0;
|
||||||
int max = 0;
|
|
||||||
int chosen = 0;
|
int chosen = 0;
|
||||||
|
int max = 0;
|
||||||
|
|
||||||
std::vector<PaidReactionTop> top;
|
std::vector<PaidReactionTop> top;
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
#include "ui/boxes/confirm_box.h"
|
#include "ui/boxes/confirm_box.h"
|
||||||
#include "ui/effects/fireworks_animation.h"
|
#include "ui/effects/fireworks_animation.h"
|
||||||
|
#include "ui/effects/premium_bubble.h"
|
||||||
#include "ui/effects/premium_graphics.h"
|
#include "ui/effects/premium_graphics.h"
|
||||||
#include "ui/layers/generic_box.h"
|
#include "ui/layers/generic_box.h"
|
||||||
#include "ui/text/text_utilities.h"
|
#include "ui/text/text_utilities.h"
|
||||||
|
@ -811,7 +812,7 @@ void FillBoostLimit(
|
||||||
st::boostBubble,
|
st::boostBubble,
|
||||||
std::move(showFinished),
|
std::move(showFinished),
|
||||||
rpl::duplicate(bubbleRowState),
|
rpl::duplicate(bubbleRowState),
|
||||||
true,
|
Premium::BubbleType::Premium,
|
||||||
nullptr,
|
nullptr,
|
||||||
&st::premiumIconBoost,
|
&st::premiumIconBoost,
|
||||||
limitLinePadding);
|
limitLinePadding);
|
||||||
|
|
|
@ -366,12 +366,18 @@ boostFeatureStories: icon{{ "settings/premium/features/feature_stories", windowB
|
||||||
boostFeatureTranscribe: icon{{ "settings/premium/features/feature_voice", windowBgActive }};
|
boostFeatureTranscribe: icon{{ "settings/premium/features/feature_voice", windowBgActive }};
|
||||||
boostFeatureOffSponsored: icon{{ "settings/premium/features/feature_off_sponsored", windowBgActive }};
|
boostFeatureOffSponsored: icon{{ "settings/premium/features/feature_off_sponsored", windowBgActive }};
|
||||||
|
|
||||||
paidReactTitleSkip: 23px;
|
paidReactBox: Box(boostBox) {
|
||||||
paidReactTopTitleMargin: margins(10px, 26px, 10px, 12px);
|
buttonPadding: margins(22px, 22px, 22px, 22px);
|
||||||
paidReactTopMargin: margins(0px, 12px, 0px, 11px);
|
buttonHeight: 42px;
|
||||||
paidReactTopUserpic: 42px;
|
button: RoundButton(defaultActiveButton) {
|
||||||
paidReactTopNameSkip: 47px;
|
height: 42px;
|
||||||
paidReactTopBadgeSkip: 32px;
|
textTop: 12px;
|
||||||
|
font: font(13px semibold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paidReactBubbleIcon: icon{{ "settings/premium/star", premiumButtonFg }};
|
||||||
|
paidReactBubbleTop: 5px;
|
||||||
|
paidReactSliderTop: 5px;
|
||||||
paidReactSlider: MediaSlider(defaultContinuousSlider) {
|
paidReactSlider: MediaSlider(defaultContinuousSlider) {
|
||||||
activeFg: creditsBg3;
|
activeFg: creditsBg3;
|
||||||
inactiveFg: creditsBg2;
|
inactiveFg: creditsBg2;
|
||||||
|
@ -382,12 +388,9 @@ paidReactSlider: MediaSlider(defaultContinuousSlider) {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
seekSize: size(16px, 16px);
|
seekSize: size(16px, 16px);
|
||||||
}
|
}
|
||||||
paidReactBox: Box(boostBox) {
|
paidReactTitleSkip: 23px;
|
||||||
buttonPadding: margins(22px, 22px, 22px, 22px);
|
paidReactTopTitleMargin: margins(10px, 26px, 10px, 12px);
|
||||||
buttonHeight: 42px;
|
paidReactTopMargin: margins(0px, 12px, 0px, 11px);
|
||||||
button: RoundButton(defaultActiveButton) {
|
paidReactTopUserpic: 42px;
|
||||||
height: 42px;
|
paidReactTopNameSkip: 47px;
|
||||||
textTop: 12px;
|
paidReactTopBadgeSkip: 32px;
|
||||||
font: font(13px semibold);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
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/animations.h"
|
||||||
#include "ui/effects/gradient.h"
|
#include "ui/effects/gradient.h"
|
||||||
#include "ui/effects/numbers_animation.h"
|
#include "ui/effects/numbers_animation.h"
|
||||||
|
#include "ui/effects/premium_bubble.h"
|
||||||
#include "ui/text/text_utilities.h"
|
#include "ui/text/text_utilities.h"
|
||||||
#include "ui/layers/generic_box.h"
|
#include "ui/layers/generic_box.h"
|
||||||
#include "ui/text/text_options.h"
|
#include "ui/text/text_options.h"
|
||||||
|
@ -35,17 +36,6 @@ namespace Ui {
|
||||||
namespace Premium {
|
namespace Premium {
|
||||||
namespace {
|
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 {
|
class GradientRadioView : public Ui::RadioView {
|
||||||
public:
|
public:
|
||||||
GradientRadioView(
|
GradientRadioView(
|
||||||
|
@ -114,36 +104,6 @@ void GradientRadioView::setBrush(std::optional<QBrush> brush) {
|
||||||
_brushOverride = 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 {
|
class PartialGradient final {
|
||||||
public:
|
public:
|
||||||
PartialGradient(int from, int to, QGradientStops stops);
|
PartialGradient(int from, int to, QGradientStops stops);
|
||||||
|
@ -183,465 +143,6 @@ QLinearGradient PartialGradient::compute(int position, int size) const {
|
||||||
return resultGradient;
|
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 {
|
class Line final : public Ui::RpWidget {
|
||||||
public:
|
public:
|
||||||
Line(
|
Line(
|
||||||
|
@ -747,7 +248,7 @@ Line::Line(
|
||||||
const auto from = state.animateFromZero
|
const auto from = state.animateFromZero
|
||||||
? 0.
|
? 0.
|
||||||
: _animation.value(_ratio);
|
: _animation.value(_ratio);
|
||||||
const auto duration = kSlideDuration * kStepBeforeDeflection;
|
const auto duration = Bubble::SlideNoDeflectionDuration();
|
||||||
_animation.start([=] {
|
_animation.start([=] {
|
||||||
update();
|
update();
|
||||||
}, from, state.ratio, duration, anim::easeOutCirc);
|
}, from, state.ratio, duration, anim::easeOutCirc);
|
||||||
|
@ -969,59 +470,6 @@ QImage GenerateStarForLightTopBar(QRectF rect) {
|
||||||
return frame;
|
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(
|
void AddLimitRow(
|
||||||
not_null<Ui::VerticalLayout*> parent,
|
not_null<Ui::VerticalLayout*> parent,
|
||||||
const style::PremiumLimits &st,
|
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(
|
void ShowListBox(
|
||||||
not_null<Ui::GenericBox*> box,
|
not_null<Ui::GenericBox*> box,
|
||||||
const style::PremiumLimits &st,
|
const style::PremiumLimits &st,
|
||||||
|
|
|
@ -45,33 +45,6 @@ inline constexpr auto kLimitRowRatio = 0.5;
|
||||||
[[nodiscard]] QByteArray ColorizedSvg(const QGradientStops &gradientStops);
|
[[nodiscard]] QByteArray ColorizedSvg(const QGradientStops &gradientStops);
|
||||||
[[nodiscard]] QImage GenerateStarForLightTopBar(QRectF rect);
|
[[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(
|
void AddLimitRow(
|
||||||
not_null<Ui::VerticalLayout*> parent,
|
not_null<Ui::VerticalLayout*> parent,
|
||||||
const style::PremiumLimits &st,
|
const style::PremiumLimits &st,
|
||||||
|
@ -130,6 +103,11 @@ void AddAccountsRow(
|
||||||
[[nodiscard]] QGradientStops GiftGradientStops();
|
[[nodiscard]] QGradientStops GiftGradientStops();
|
||||||
[[nodiscard]] QGradientStops CreditsIconGradientStops();
|
[[nodiscard]] QGradientStops CreditsIconGradientStops();
|
||||||
|
|
||||||
|
[[nodiscard]] QLinearGradient ComputeGradient(
|
||||||
|
not_null<QWidget*> content,
|
||||||
|
int left,
|
||||||
|
int width);
|
||||||
|
|
||||||
struct ListEntry final {
|
struct ListEntry final {
|
||||||
rpl::producer<QString> title;
|
rpl::producer<QString> title;
|
||||||
rpl::producer<TextWithEntities> about;
|
rpl::producer<TextWithEntities> about;
|
||||||
|
|
|
@ -378,6 +378,8 @@ PRIVATE
|
||||||
ui/effects/loading_element.h
|
ui/effects/loading_element.h
|
||||||
ui/effects/outline_segments.cpp
|
ui/effects/outline_segments.cpp
|
||||||
ui/effects/outline_segments.h
|
ui/effects/outline_segments.h
|
||||||
|
ui/effects/premium_bubble.cpp
|
||||||
|
ui/effects/premium_bubble.h
|
||||||
ui/effects/premium_graphics.cpp
|
ui/effects/premium_graphics.cpp
|
||||||
ui/effects/premium_graphics.h
|
ui/effects/premium_graphics.h
|
||||||
ui/effects/premium_stars.cpp
|
ui/effects/premium_stars.cpp
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit a5b1266a8c340ed916466a83cbbc5793471c2438
|
Subproject commit 95229cd46bbba42b431a097705494ec39cce5f0c
|
Loading…
Add table
Reference in a new issue