mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-08-22 10:17:10 +00:00
510 lines
14 KiB
C++
510 lines
14 KiB
C++
/*
|
|
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/controls/stars_rating.h"
|
|
|
|
#include "info/profile/info_profile_icon.h"
|
|
#include "lang/lang_keys.h"
|
|
#include "ui/effects/premium_bubble.h"
|
|
#include "ui/effects/premium_graphics.h"
|
|
#include "ui/layers/generic_box.h"
|
|
#include "ui/layers/show.h"
|
|
#include "ui/text/custom_emoji_helper.h"
|
|
#include "ui/text/text_utilities.h"
|
|
#include "ui/toast/toast.h"
|
|
#include "ui/widgets/buttons.h"
|
|
#include "ui/widgets/labels.h"
|
|
#include "ui/widgets/tooltip.h"
|
|
#include "ui/painter.h"
|
|
#include "ui/rp_widget.h"
|
|
#include "ui/ui_utility.h"
|
|
#include "styles/style_info.h"
|
|
#include "styles/style_layers.h"
|
|
#include "styles/style_premium.h"
|
|
#include "styles/style_settings.h"
|
|
#include "styles/style_media_view.h"
|
|
#include "styles/style_menu_icons.h"
|
|
|
|
namespace Ui {
|
|
namespace {
|
|
|
|
constexpr auto kAutoCollapseTimeout = 4 * crl::time(1000);
|
|
|
|
using Counters = Data::StarsRating;
|
|
|
|
struct Feature {
|
|
const style::icon &icon;
|
|
QString title;
|
|
TextWithEntities about;
|
|
};
|
|
|
|
[[nodiscard]] object_ptr<Ui::RpWidget> MakeFeature(
|
|
QWidget *parent,
|
|
Feature feature,
|
|
const Text::MarkedContext &context) {
|
|
auto result = object_ptr<Ui::PaddingWrap<>>(
|
|
parent,
|
|
object_ptr<Ui::RpWidget>(parent),
|
|
st::infoStarsFeatureMargin);
|
|
const auto widget = result->entity();
|
|
const auto icon = Ui::CreateChild<Info::Profile::FloatingIcon>(
|
|
widget,
|
|
feature.icon,
|
|
st::infoStarsFeatureIconPosition);
|
|
const auto title = Ui::CreateChild<Ui::FlatLabel>(
|
|
widget,
|
|
feature.title,
|
|
st::infoStarsFeatureTitle);
|
|
const auto about = Ui::CreateChild<Ui::FlatLabel>(
|
|
widget,
|
|
rpl::single(feature.about),
|
|
st::infoStarsFeatureAbout,
|
|
st::defaultPopupMenu,
|
|
context);
|
|
icon->show();
|
|
title->show();
|
|
about->show();
|
|
widget->widthValue(
|
|
) | rpl::start_with_next([=](int width) {
|
|
const auto left = st::infoStarsFeatureLabelLeft;
|
|
const auto available = width - left;
|
|
title->resizeToWidth(available);
|
|
about->resizeToWidth(available);
|
|
auto top = 0;
|
|
title->move(left, top);
|
|
top += title->height() + st::infoStarsFeatureSkip;
|
|
about->move(left, top);
|
|
top += about->height();
|
|
widget->resize(width, top);
|
|
}, widget->lifetime());
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] Fn<QImage()> CustomEmojiBadgeFactory(
|
|
const QString &text,
|
|
const style::color &bg,
|
|
const style::color &fg) {
|
|
return [=] {
|
|
auto string = Ui::Text::String(
|
|
st::settingsPremiumNewBadge.style,
|
|
text.toUpper());
|
|
const auto size = QSize(string.maxWidth(), string.minHeight());
|
|
const auto padding = st::settingsPremiumNewBadgePadding;
|
|
const auto full = size.grownBy(padding);
|
|
const auto ratio = style::DevicePixelRatio();
|
|
|
|
auto result = QImage(
|
|
full * ratio,
|
|
QImage::Format_ARGB32_Premultiplied);
|
|
result.setDevicePixelRatio(ratio);
|
|
result.fill(Qt::transparent);
|
|
|
|
auto p = QPainter(&result);
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(bg);
|
|
|
|
const auto r = padding.left();
|
|
p.drawRoundedRect(0, 0, full.width(), full.height(), r, r);
|
|
|
|
p.setPen(fg);
|
|
string.draw(p, { .position = { padding.left(), padding.top() } });
|
|
|
|
p.end();
|
|
return result;
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] Counters AdjustByReached(Counters data) {
|
|
const auto reached = !data.nextLevelStars;
|
|
if (reached) {
|
|
--data.level;
|
|
data.stars = data.nextLevelStars = std::max({
|
|
data.stars,
|
|
data.thisLevelStars,
|
|
1
|
|
});
|
|
data.thisLevelStars = 0;
|
|
} else {
|
|
data.stars = std::max(data.thisLevelStars, data.stars);
|
|
data.nextLevelStars = std::max(
|
|
data.nextLevelStars,
|
|
data.stars + 1);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
[[nodiscard]] Fn<QString(int)> BubbleTextFactory(int countForScale) {
|
|
return [=](int count) {
|
|
return (countForScale < 10'000)
|
|
? QString::number(count)
|
|
: (countForScale < 10'000'000)
|
|
? (QString::number((count / 100) / 10.) + 'K')
|
|
: (QString::number((count / 100'000) / 10.) + 'M');
|
|
};
|
|
}
|
|
|
|
void FillRatingLimit(
|
|
rpl::producer<> showFinished,
|
|
not_null<VerticalLayout*> container,
|
|
rpl::producer<Counters> data,
|
|
style::margins limitLinePadding,
|
|
int starsForScale) {
|
|
const auto addSkip = [&](int skip) {
|
|
container->add(object_ptr<Ui::FixedHeightWidget>(container, skip));
|
|
};
|
|
|
|
const auto ratio = [=](Counters rating) {
|
|
const auto min = rating.thisLevelStars;
|
|
const auto max = rating.nextLevelStars;
|
|
|
|
Assert(rating.stars >= min && rating.stars <= max);
|
|
const auto count = (max - min);
|
|
const auto index = (rating.stars - min);
|
|
if (!index) {
|
|
return 0.;
|
|
} else if (index == count) {
|
|
return 1.;
|
|
} else if (count == 2) {
|
|
return 0.5;
|
|
}
|
|
const auto available = st::boxWideWidth
|
|
- st::boxPadding.left()
|
|
- st::boxPadding.right();
|
|
const auto average = available / float64(count);
|
|
const auto levelWidth = [&](int add) {
|
|
return st::normalFont->width(
|
|
tr::lng_boost_level(
|
|
tr::now,
|
|
lt_count,
|
|
rating.level + add));
|
|
};
|
|
const auto paddings = 2 * st::premiumLineTextSkip;
|
|
const auto labelLeftWidth = paddings + levelWidth(0);
|
|
const auto labelRightWidth = paddings + levelWidth(1);
|
|
const auto first = std::max(average, labelLeftWidth * 1.);
|
|
const auto last = std::max(average, labelRightWidth * 1.);
|
|
const auto other = (available - first - last) / (count - 2);
|
|
return (first + (index - 1) * other) / available;
|
|
};
|
|
|
|
auto adjustedData = rpl::duplicate(data) | rpl::map(AdjustByReached);
|
|
|
|
auto bubbleRowState = rpl::duplicate(
|
|
adjustedData
|
|
) | rpl::combine_previous(
|
|
Counters()
|
|
) | rpl::map([=](Counters previous, Counters counters) {
|
|
return Premium::BubbleRowState{
|
|
.counter = counters.stars,
|
|
.ratio = ratio(counters),
|
|
.animateFromZero = (counters.level != previous.level),
|
|
.dynamic = true,
|
|
};
|
|
});
|
|
Premium::AddBubbleRow(
|
|
container,
|
|
st::boostBubble,
|
|
std::move(showFinished),
|
|
rpl::duplicate(bubbleRowState),
|
|
Premium::BubbleType::StarRating,
|
|
BubbleTextFactory(starsForScale),
|
|
&st::infoStarsCrown,
|
|
limitLinePadding);
|
|
addSkip(st::premiumLineTextSkip);
|
|
|
|
const auto level = [](int level) {
|
|
return tr::lng_boost_level(tr::now, lt_count, level);
|
|
};
|
|
auto limitState = std::move(
|
|
bubbleRowState
|
|
) | rpl::map([](const Premium::BubbleRowState &state) {
|
|
return Premium::LimitRowState{
|
|
.ratio = state.ratio,
|
|
.animateFromZero = state.animateFromZero,
|
|
.dynamic = state.dynamic
|
|
};
|
|
});
|
|
auto left = rpl::duplicate(
|
|
adjustedData
|
|
) | rpl::map([=](Counters counters) {
|
|
return level(counters.level);
|
|
});
|
|
auto right = rpl::duplicate(
|
|
adjustedData
|
|
) | rpl::map([=](Counters counters) {
|
|
return level(counters.level + 1);
|
|
});
|
|
Premium::AddLimitRow(
|
|
container,
|
|
st::boostLimits,
|
|
Premium::LimitRowLabels{
|
|
.leftLabel = std::move(left),
|
|
.rightLabel = std::move(right),
|
|
.activeLineBg = [=] { return st::windowBgActive->b; },
|
|
},
|
|
std::move(limitState),
|
|
limitLinePadding);
|
|
}
|
|
|
|
object_ptr<Ui::FlatLabel> MakeBoostFeaturesBadge(
|
|
not_null<QWidget*> parent,
|
|
rpl::producer<QString> text,
|
|
Fn<QBrush(QRect)> bg) {
|
|
auto result = object_ptr<Ui::FlatLabel>(
|
|
parent,
|
|
std::move(text),
|
|
st::boostLevelBadge);
|
|
const auto label = result.data();
|
|
|
|
label->show();
|
|
label->paintRequest() | rpl::start_with_next([=] {
|
|
const auto size = label->textMaxWidth();
|
|
const auto rect = QRect(
|
|
(label->width() - size) / 2,
|
|
st::boostLevelBadge.margin.top(),
|
|
size,
|
|
st::boostLevelBadge.style.font->height
|
|
).marginsAdded(st::boostLevelBadge.margin);
|
|
auto p = QPainter(label);
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setBrush(bg(rect));
|
|
p.setPen(Qt::NoPen);
|
|
p.drawRoundedRect(rect, rect.height() / 2., rect.height() / 2.);
|
|
|
|
const auto &lineFg = st::windowBgRipple;
|
|
const auto line = st::boostLevelBadgeLine;
|
|
const auto top = st::boostLevelBadge.margin.top()
|
|
+ ((st::boostLevelBadge.style.font->height - line) / 2);
|
|
const auto left = 0;
|
|
const auto skip = st::boostLevelBadgeSkip;
|
|
if (const auto right = rect.x() - skip; right > left) {
|
|
p.fillRect(left, top, right - left, line, lineFg);
|
|
}
|
|
const auto right = label->width();
|
|
if (const auto left = rect.x() + rect.width() + skip
|
|
; left < right) {
|
|
p.fillRect(left, top, right - left, line, lineFg);
|
|
}
|
|
}, label->lifetime());
|
|
|
|
return result;
|
|
}
|
|
|
|
void AboutRatingBox(
|
|
not_null<GenericBox*> box,
|
|
const QString &name,
|
|
Counters data) {
|
|
box->setWidth(st::boxWideWidth);
|
|
box->setStyle(st::boostBox);
|
|
|
|
struct State {
|
|
rpl::variable<Counters> data;
|
|
rpl::variable<bool> full;
|
|
};
|
|
const auto state = box->lifetime().make_state<State>();
|
|
state->data = std::move(data);
|
|
|
|
FillRatingLimit(
|
|
BoxShowFinishes(box),
|
|
box->verticalLayout(),
|
|
state->data.value(),
|
|
st::boxRowPadding,
|
|
data.stars);
|
|
|
|
box->setMaxHeight(st::boostBoxMaxHeight);
|
|
const auto close = box->addTopButton(
|
|
st::boxTitleClose,
|
|
[=] { box->closeBox(); });
|
|
|
|
auto title = tr::lng_stars_rating_title();;
|
|
|
|
auto text = !name.isEmpty()
|
|
? tr::lng_stars_rating_about(
|
|
lt_name,
|
|
rpl::single(TextWithEntities{ name }),
|
|
Ui::Text::RichLangValue) | rpl::type_erased()
|
|
: tr::lng_stars_rating_about_your(
|
|
Ui::Text::RichLangValue) | rpl::type_erased();
|
|
|
|
box->addRow(
|
|
object_ptr<Ui::FlatLabel>(box, std::move(title), st::infoStarsTitle),
|
|
st::boxRowPadding + QMargins(0, st::boostTitleSkip / 2, 0, 0));
|
|
|
|
const auto aboutLabel = box->addRow(
|
|
object_ptr<Ui::FlatLabel>(
|
|
box,
|
|
std::move(text),
|
|
st::boostText),
|
|
(st::boxRowPadding
|
|
+ QMargins(0, st::boostTextSkip, 0, st::boostBottomSkip)));
|
|
aboutLabel->setTryMakeSimilarLines(true);
|
|
|
|
auto helper = Ui::Text::CustomEmojiHelper();
|
|
const auto makeBadge = [&](
|
|
const QString &text,
|
|
const style::color &bg,
|
|
const style::color &fg) {
|
|
return helper.paletteDependent(
|
|
CustomEmojiBadgeFactory(text, bg, fg),
|
|
st::badgeEmojiMargin);
|
|
};
|
|
const auto makeActive = [&](const QString &text) {
|
|
return makeBadge(text, st::windowBgActive, st::windowFgActive);
|
|
};
|
|
const auto makeInactive = [&](const QString &text) {
|
|
return makeBadge(text, st::windowSubTextFg, st::windowFgActive);
|
|
};
|
|
const auto features = std::vector<Feature>{
|
|
{
|
|
st::menuIconRatingGifts,
|
|
tr::lng_stars_title_gifts_telegram(tr::now),
|
|
tr::lng_stars_about_gifts_telegram(
|
|
tr::now,
|
|
lt_emoji,
|
|
makeActive(tr::lng_stars_rating_added(tr::now)),
|
|
Ui::Text::RichLangValue),
|
|
},
|
|
{
|
|
st::menuIconRatingUsers,
|
|
tr::lng_stars_title_gifts_users(tr::now),
|
|
tr::lng_stars_about_gifts_users(
|
|
tr::now,
|
|
lt_emoji,
|
|
makeActive(tr::lng_stars_rating_added(tr::now)),
|
|
Ui::Text::RichLangValue),
|
|
},
|
|
{
|
|
st::menuIconRatingRefund,
|
|
tr::lng_stars_title_refunds(tr::now),
|
|
tr::lng_stars_about_refunds(
|
|
tr::now,
|
|
lt_emoji,
|
|
makeInactive(tr::lng_stars_rating_deducted(tr::now)),
|
|
Ui::Text::RichLangValue),
|
|
},
|
|
};
|
|
const auto context = helper.context();
|
|
for (const auto &feature : features) {
|
|
box->addRow(MakeFeature(box, feature, context));
|
|
}
|
|
box->addButton(rpl::single(QString()), [=] {
|
|
box->closeBox();
|
|
})->setText(rpl::single(Ui::Text::IconEmoji(
|
|
&st::infoStarsUnderstood
|
|
).append(' ').append(tr::lng_stars_rating_understood(tr::now))));
|
|
}
|
|
|
|
} // namespace
|
|
|
|
StarsRating::StarsRating(
|
|
QWidget *parent,
|
|
const style::StarsRating &st,
|
|
std::shared_ptr<Ui::Show> show,
|
|
const QString &name,
|
|
rpl::producer<Counters> value)
|
|
: _widget(std::make_unique<Ui::AbstractButton>(parent))
|
|
, _st(st)
|
|
, _show(std::move(show))
|
|
, _name(name)
|
|
, _value(std::move(value)) {
|
|
init();
|
|
}
|
|
|
|
StarsRating::~StarsRating() = default;
|
|
|
|
void StarsRating::init() {
|
|
_widget->setPointerCursor(true);
|
|
|
|
_widget->paintRequest() | rpl::start_with_next([=] {
|
|
auto p = QPainter(_widget.get());
|
|
paint(p);
|
|
}, lifetime());
|
|
|
|
_widget->setClickedCallback([=] {
|
|
if (!_value.current()) {
|
|
return;
|
|
}
|
|
_show->show(Box(AboutRatingBox, _name, _value.current()));
|
|
});
|
|
|
|
const auto added = _st.margin + _st.padding;
|
|
const auto fontHeight = _st.style.font->height;
|
|
const auto height = added.top() + fontHeight + added.bottom();
|
|
_widget->resize(_widget->width(), height);
|
|
|
|
_value.value() | rpl::start_with_next([=](Counters rating) {
|
|
if (!rating) {
|
|
_widget->resize(0, _widget->height());
|
|
_collapsedWidthValue = 0;
|
|
return;
|
|
}
|
|
updateTexts(rating);
|
|
}, lifetime());
|
|
}
|
|
|
|
void StarsRating::updateTexts(Counters rating) {
|
|
_collapsedText.setText(
|
|
_st.style,
|
|
Lang::FormatCountDecimal(rating.level));
|
|
|
|
const auto added = _st.padding;
|
|
const auto add = added.left() + added.right();
|
|
const auto height = _widget->height();
|
|
_collapsedWidthValue = _st.margin.right()
|
|
+ std::max(
|
|
add + _collapsedText.maxWidth(),
|
|
height - _st.margin.top() - _st.margin.bottom());
|
|
updateWidth();
|
|
}
|
|
|
|
void StarsRating::updateWidth() {
|
|
const auto widthToRight = _collapsedWidthValue.current();
|
|
_widget->resize(_st.margin.left() + widthToRight, _widget->height());
|
|
_widget->update();
|
|
}
|
|
|
|
void StarsRating::raise() {
|
|
_widget->raise();
|
|
}
|
|
|
|
void StarsRating::moveTo(int x, int y) {
|
|
_widget->move(x - _st.margin.left(), y - _st.margin.top());
|
|
}
|
|
|
|
void StarsRating::paint(QPainter &p) {
|
|
const auto outer = _widget->rect().marginsRemoved(_st.margin);
|
|
if (outer.isEmpty()) {
|
|
return;
|
|
}
|
|
const auto inner = outer.marginsRemoved(_st.padding);
|
|
|
|
auto hq = PainterHighQualityEnabler(p);
|
|
p.setPen(Qt::NoPen);
|
|
p.setBrush(_st.activeBg);
|
|
|
|
const auto value = _value.current();
|
|
const auto radius = outer.height() / 2.;
|
|
p.drawRoundedRect(outer, radius, radius);
|
|
p.setPen(_st.activeFg);
|
|
const auto skip = (inner.width() - _collapsedText.maxWidth()) / 2;
|
|
_collapsedText.draw(p, {
|
|
.position = inner.topLeft() + QPoint(skip, 0),
|
|
.availableWidth = _collapsedText.maxWidth(),
|
|
});
|
|
}
|
|
|
|
rpl::producer<int> StarsRating::collapsedWidthValue() const {
|
|
return _collapsedWidthValue.value();
|
|
}
|
|
|
|
rpl::lifetime &StarsRating::lifetime() {
|
|
return _widget->lifetime();
|
|
}
|
|
|
|
} // namespace Ui
|