Improve paid reactions box design.

This commit is contained in:
John Preston 2024-08-07 18:09:32 +02:00
parent 273e041935
commit 02610de010
13 changed files with 875 additions and 641 deletions

View file

@ -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}.";

View file

@ -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);

View file

@ -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),

View file

@ -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(

View file

@ -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;

View file

@ -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);

View file

@ -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;

View 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

View 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

View file

@ -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,

View file

@ -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;

View file

@ -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