/* 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_graphics.h" #include "data/data_subscription_option.h" #include "lang/lang_keys.h" #include "ui/abstract_button.h" #include "ui/effects/animations.h" #include "ui/effects/gradient.h" #include "ui/effects/numbers_animation.h" #include "ui/text/text_utilities.h" #include "ui/layers/generic_box.h" #include "ui/text/text_options.h" #include "ui/widgets/checkbox.h" #include "ui/wrap/padding_wrap.h" #include "ui/wrap/vertical_layout.h" #include "ui/painter.h" #include "ui/rect.h" #include "styles/style_boxes.h" #include "styles/style_layers.h" #include "styles/style_premium.h" #include "styles/style_settings.h" #include "styles/style_window.h" #include #include #include namespace Ui { namespace Premium { namespace { using TextFactory = Fn; constexpr auto kBubbleRadiusSubtractor = 2; constexpr auto kDeflectionSmall = 20.; constexpr auto kDeflection = 30.; constexpr auto kSlideDuration = crl::time(1000); constexpr auto kStepBeforeDeflection = 0.75; constexpr auto kStepAfterDeflection = kStepBeforeDeflection + (1. - kStepBeforeDeflection) / 2.; class GradientRadioView : public Ui::RadioView { public: GradientRadioView( const style::Radio &st, bool checked, Fn updateCallback = nullptr); void setBrush(std::optional brush); void paint(QPainter &p, int left, int top, int outerWidth) override; private: not_null _st; std::optional _brushOverride; }; GradientRadioView::GradientRadioView( const style::Radio &st, bool checked, Fn updateCallback) : Ui::RadioView(st, checked, updateCallback) , _st(&st) { } void GradientRadioView::paint(QPainter &p, int left, int top, int outerW) { auto hq = PainterHighQualityEnabler(p); const auto toggled = currentAnimationValue(); const auto toggledFg = _brushOverride ? (*_brushOverride) : QBrush(_st->toggledFg); { const auto skip = (_st->outerSkip / 10.) + (_st->thickness / 2); const auto rect = QRectF(left, top, _st->diameter, _st->diameter) - Margins(skip); p.setBrush(_st->bg); if (toggled < 1) { p.setPen(QPen(_st->untoggledFg, _st->thickness)); p.drawEllipse(style::rtlrect(rect, outerW)); } if (toggled > 0) { p.setOpacity(toggled); p.setPen(QPen(toggledFg, _st->thickness)); p.drawEllipse(style::rtlrect(rect, outerW)); } } if (toggled > 0) { p.setPen(Qt::NoPen); p.setBrush(toggledFg); const auto skip0 = _st->diameter / 2.; const auto skip1 = _st->skip / 10.; const auto checkSkip = skip0 * (1. - toggled) + skip1 * toggled; const auto rect = QRectF(left, top, _st->diameter, _st->diameter) - Margins(checkSkip); p.drawEllipse(style::rtlrect(rect, outerW)); } } void GradientRadioView::setBrush(std::optional brush) { _brushOverride = brush; } [[nodiscard]] TextFactory ProcessTextFactory( std::optional> phrase) { return phrase ? TextFactory([=](int n) { return (*phrase)(tr::now, lt_count, n); }) : TextFactory([=](int n) { return QString::number(n); }); } [[nodiscard]] QLinearGradient ComputeGradient( not_null content, int left, int width) { // Take a full width of parent box without paddings. const auto fullGradientWidth = content->parentWidget()->width(); auto fullGradient = QLinearGradient(0, 0, fullGradientWidth, 0); fullGradient.setStops(ButtonGradientStops()); auto gradient = QLinearGradient(0, 0, width, 0); const auto fullFinal = float64(fullGradient.finalStop().x()); left += ((fullGradientWidth - content->width()) / 2); gradient.setColorAt( .0, anim::gradient_color_at(fullGradient, left / fullFinal)); gradient.setColorAt( 1., anim::gradient_color_at(fullGradient, (left + width) / fullFinal)); return gradient; } class PartialGradient final { public: PartialGradient(int from, int to, QGradientStops stops); [[nodiscard]] QLinearGradient compute(int position, int size) const; private: const int _from; const int _to; QLinearGradient _gradient; }; PartialGradient::PartialGradient(int from, int to, QGradientStops stops) : _from(from) , _to(to) , _gradient(0, 0, 0, to - from) { _gradient.setStops(std::move(stops)); } QLinearGradient PartialGradient::compute(int position, int size) const { const auto pointTop = position - _from; const auto pointBottom = pointTop + size; const auto ratioTop = pointTop / float64(_to - _from); const auto ratioBottom = pointBottom / float64(_to - _from); auto resultGradient = QLinearGradient( QPointF(), QPointF(0, pointBottom - pointTop)); resultGradient.setColorAt( .0, anim::gradient_color_at(_gradient, ratioTop)); resultGradient.setColorAt( .1, anim::gradient_color_at(_gradient, ratioBottom)); return resultGradient; } class Bubble final { public: using EdgeProgress = float64; Bubble( const style::PremiumBubble &st, Fn updateCallback, TextFactory textFactory, const style::icon *icon, bool premiumPossible); [[nodiscard]] int counter() const; [[nodiscard]] int height() const; [[nodiscard]] int width() const; [[nodiscard]] int bubbleRadius() const; [[nodiscard]] int countMaxWidth(int maxPossibleCounter) const; void setCounter(int value); void setTailEdge(EdgeProgress edge); void setFlipHorizontal(bool value); void paintBubble(QPainter &p, const QRect &r, const QBrush &brush); [[nodiscard]] rpl::producer<> widthChanges() const; private: [[nodiscard]] int filledWidth() const; const style::PremiumBubble &_st; const Fn _updateCallback; const TextFactory _textFactory; const style::icon *_icon; NumbersAnimation _numberAnimation; const int _height; const int _textTop; const bool _premiumPossible; int _counter = -1; EdgeProgress _tailEdge = 0.; bool _flipHorizontal = false; rpl::event_stream<> _widthChanges; }; Bubble::Bubble( const style::PremiumBubble &st, Fn updateCallback, TextFactory textFactory, const style::icon *icon, bool premiumPossible) : _st(st) , _updateCallback(std::move(updateCallback)) , _textFactory(std::move(textFactory)) , _icon(icon) , _numberAnimation(_st.font, _updateCallback) , _height(_st.height + _st.tailSize.height()) , _textTop((_height - _st.tailSize.height() - _st.font->height) / 2) , _premiumPossible(premiumPossible) { _numberAnimation.setDisabledMonospace(true); _numberAnimation.setWidthChangedCallback([=] { _widthChanges.fire({}); }); _numberAnimation.setText(_textFactory(0), 0); _numberAnimation.finishAnimating(); } int Bubble::counter() const { return _counter; } int Bubble::height() const { return _height; } int Bubble::bubbleRadius() const { return (_height - _st.tailSize.height()) / 2 - kBubbleRadiusSubtractor; } int Bubble::filledWidth() const { return _st.padding.left() + _icon->width() + _st.textSkip + _st.padding.right(); } int Bubble::width() const { return filledWidth() + _numberAnimation.countWidth(); } int Bubble::countMaxWidth(int maxPossibleCounter) const { auto numbers = Ui::NumbersAnimation(_st.font, [] {}); numbers.setDisabledMonospace(true); numbers.setDuration(0); numbers.setText(_textFactory(0), 0); numbers.setText(_textFactory(maxPossibleCounter), maxPossibleCounter); numbers.finishAnimating(); return filledWidth() + numbers.maxWidth(); } void Bubble::setCounter(int value) { if (_counter != value) { _counter = value; _numberAnimation.setText(_textFactory(_counter), _counter); } } void Bubble::setTailEdge(EdgeProgress edge) { _tailEdge = std::clamp(edge, 0., 1.); } void Bubble::setFlipHorizontal(bool value) { _flipHorizontal = value; } void Bubble::paintBubble(QPainter &p, const QRect &r, const QBrush &brush) { if (_counter < 0) { return; } const auto penWidth = _st.penWidth; const auto penWidthHalf = penWidth / 2; const auto bubbleRect = r - style::margins( penWidthHalf, penWidthHalf, penWidthHalf, _st.tailSize.height() + penWidthHalf); { const auto radius = bubbleRadius(); auto pathTail = QPainterPath(); const auto tailWHalf = _st.tailSize.width() / 2.; const auto progress = _tailEdge; const auto tailTop = bubbleRect.y() + bubbleRect.height(); const auto tailLeftFull = bubbleRect.x() + (bubbleRect.width() * 0.5) - tailWHalf; const auto tailLeft = bubbleRect.x() + (bubbleRect.width() * 0.5 * (progress + 1.)) - tailWHalf; const auto tailCenter = tailLeft + tailWHalf; const auto tailRight = [&] { const auto max = bubbleRect.x() + bubbleRect.width(); const auto right = tailLeft + _st.tailSize.width(); const auto bottomMax = max - radius; return (right > bottomMax) ? std::max(float64(tailCenter), float64(bottomMax)) : right; }(); if (_premiumPossible) { pathTail.moveTo(tailLeftFull, tailTop); pathTail.lineTo(tailLeft, tailTop); pathTail.lineTo(tailCenter, tailTop + _st.tailSize.height()); pathTail.lineTo(tailRight, tailTop); pathTail.lineTo(tailRight, tailTop - radius); pathTail.moveTo(tailLeftFull, tailTop); } auto pathBubble = QPainterPath(); pathBubble.setFillRule(Qt::WindingFill); pathBubble.addRoundedRect(bubbleRect, radius, radius); auto hq = PainterHighQualityEnabler(p); p.setPen(QPen( brush, penWidth, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); p.setBrush(brush); if (_flipHorizontal) { auto m = QTransform(); const auto center = bubbleRect.center(); m.translate(center.x(), center.y()); m.scale(-1., 1.); m.translate(-center.x(), -center.y()); m.translate(-bubbleRect.left() + 1., 0); p.drawPath(m.map(pathTail + pathBubble)); } else { p.drawPath(pathTail + pathBubble); } } p.setPen(st::activeButtonFg); p.setFont(_st.font); const auto iconLeft = r.x() + _st.padding.left(); _icon->paint( p, iconLeft, bubbleRect.y() + (bubbleRect.height() - _icon->height()) / 2, bubbleRect.width()); _numberAnimation.paint( p, iconLeft + _icon->width() + _st.textSkip, r.y() + _textTop, width() / 2); } rpl::producer<> Bubble::widthChanges() const { return _widthChanges.events(); } class BubbleWidget final : public Ui::RpWidget { public: BubbleWidget( not_null parent, const style::PremiumBubble &st, TextFactory textFactory, rpl::producer state, bool premiumPossible, rpl::producer<> showFinishes, const style::icon *icon, const style::margins &outerPadding); protected: void paintEvent(QPaintEvent *e) override; private: struct GradientParams { int left = 0; int width = 0; int outer = 0; friend inline constexpr bool operator==( GradientParams, GradientParams) = default; }; void animateTo(BubbleRowState state); const style::PremiumBubble &_st; BubbleRowState _animatingFrom; float64 _animatingFromResultRatio = 0.; rpl::variable _state; Bubble _bubble; int _maxBubbleWidth = 0; const bool _premiumPossible; const style::margins _outerPadding; Ui::Animations::Simple _appearanceAnimation; QSize _spaceForDeflection; QLinearGradient _cachedGradient; std::optional _cachedGradientParams; float64 _deflection; bool _ignoreDeflection = false; float64 _stepBeforeDeflection; float64 _stepAfterDeflection; }; BubbleWidget::BubbleWidget( not_null parent, const style::PremiumBubble &st, TextFactory textFactory, rpl::producer state, bool premiumPossible, rpl::producer<> showFinishes, const style::icon *icon, const style::margins &outerPadding) : RpWidget(parent) , _st(st) , _state(std::move(state)) , _bubble( _st, [=] { update(); }, std::move(textFactory), icon, premiumPossible) , _premiumPossible(premiumPossible) , _outerPadding(outerPadding) , _deflection(kDeflection) , _stepBeforeDeflection(kStepBeforeDeflection) , _stepAfterDeflection(kStepAfterDeflection) { const auto resizeTo = [=](int w, int h) { _deflection = (w > _st.widthLimit) ? kDeflectionSmall : kDeflection; _spaceForDeflection = QSize(_st.skip, _st.skip); resize(QSize(w, h) + _spaceForDeflection); }; resizeTo(_bubble.width(), _bubble.height()); _bubble.widthChanges( ) | rpl::start_with_next([=] { resizeTo(_bubble.width(), _bubble.height()); }, lifetime()); std::move( showFinishes ) | rpl::take(1) | rpl::start_with_next([=] { _state.value( ) | rpl::start_with_next([=](BubbleRowState state) { animateTo(state); }, lifetime()); }, lifetime()); } void BubbleWidget::animateTo(BubbleRowState state) { _maxBubbleWidth = _bubble.countMaxWidth(state.counter); const auto parent = parentWidget(); const auto computeLeft = [=](float64 pointRatio, float64 animProgress) { const auto halfWidth = (_maxBubbleWidth / 2); const auto left = _outerPadding.left(); const auto right = _outerPadding.right(); const auto available = parent->width() - left - right; const auto delta = (pointRatio - _animatingFromResultRatio); const auto center = available * (_animatingFromResultRatio + delta * animProgress); return center - halfWidth + left; }; const auto moveEndPoint = state.ratio; const auto computeEdge = [=] { return parent->width() - _outerPadding.right() - _maxBubbleWidth; }; struct LeftEdge final { float64 goodPointRatio = 0.; float64 bubbleLeftEdge = 0.; }; const auto leftEdge = [&]() -> LeftEdge { const auto finish = computeLeft(moveEndPoint, 1.); const auto &padding = _outerPadding; if (finish <= padding.left()) { const auto halfWidth = (_maxBubbleWidth / 2); const auto goodPointRatio = float64(halfWidth) / (parent->width() - padding.left() - padding.right()); const auto bubbleLeftEdge = (padding.left() - finish) / (_maxBubbleWidth / 2.); return { goodPointRatio, bubbleLeftEdge }; } return {}; }(); const auto checkBubbleRightEdge = [&]() -> Bubble::EdgeProgress { const auto finish = computeLeft(moveEndPoint, 1.); const auto edge = computeEdge(); return (finish >= edge) ? (finish - edge) / (_maxBubbleWidth / 2.) : 0.; }; const auto bubbleRightEdge = checkBubbleRightEdge(); _ignoreDeflection = !_state.current().dynamic && (bubbleRightEdge || leftEdge.goodPointRatio); if (_ignoreDeflection) { _stepBeforeDeflection = 1.; _stepAfterDeflection = 1.; } const auto resultMoveEndPoint = leftEdge.goodPointRatio ? leftEdge.goodPointRatio : moveEndPoint; _bubble.setFlipHorizontal(leftEdge.bubbleLeftEdge); const auto duration = kSlideDuration * (_ignoreDeflection ? kStepBeforeDeflection : 1.) * ((_state.current().ratio < 0.001) ? 0.5 : 1.); if (state.animateFromZero) { _animatingFrom.ratio = 0.; _animatingFrom.counter = 0; _animatingFromResultRatio = 0.; } _appearanceAnimation.start([=](float64 value) { if (!_appearanceAnimation.animating()) { _animatingFrom = state; _animatingFromResultRatio = resultMoveEndPoint; } const auto moveProgress = std::clamp( (value / _stepBeforeDeflection), 0., 1.); const auto counterProgress = std::clamp( (value / _stepAfterDeflection), 0., 1.); moveToLeft( std::max( int(base::SafeRound( (computeLeft(resultMoveEndPoint, moveProgress) - (_maxBubbleWidth / 2.) * bubbleRightEdge))), 0), 0); const auto now = _animatingFrom.counter + counterProgress * (state.counter - _animatingFrom.counter); _bubble.setCounter(int(base::SafeRound(now))); const auto edgeProgress = leftEdge.bubbleLeftEdge ? leftEdge.bubbleLeftEdge : (bubbleRightEdge * value); _bubble.setTailEdge(edgeProgress); update(); }, 0., 1., duration, anim::easeOutCirc); } void BubbleWidget::paintEvent(QPaintEvent *e) { if (_bubble.counter() < 0) { return; } auto p = QPainter(this); const auto padding = QMargins( 0, _spaceForDeflection.height(), _spaceForDeflection.width(), 0); const auto bubbleRect = rect() - padding; const auto params = GradientParams{ .left = x(), .width = bubbleRect.width(), .outer = parentWidget()->parentWidget()->width(), }; if (_cachedGradientParams != params) { _cachedGradient = ComputeGradient( parentWidget(), params.left, params.width); _cachedGradientParams = params; } if (_appearanceAnimation.animating()) { const auto progress = _appearanceAnimation.value(1.); const auto finalScale = (_animatingFromResultRatio > 0.) || (_state.current().ratio < 0.001); const auto scaleProgress = finalScale ? 1. : std::clamp((progress / _stepBeforeDeflection), 0., 1.); const auto scale = scaleProgress; const auto rotationProgress = std::clamp( (progress - _stepBeforeDeflection) / (1. - _stepBeforeDeflection), 0., 1.); const auto rotationProgressReverse = std::clamp( (progress - _stepAfterDeflection) / (1. - _stepAfterDeflection), 0., 1.); const auto offsetX = bubbleRect.x() + bubbleRect.width() / 2; const auto offsetY = bubbleRect.y() + bubbleRect.height(); p.translate(offsetX, offsetY); p.scale(scale, scale); if (!_ignoreDeflection) { p.rotate(rotationProgress * _deflection - rotationProgressReverse * _deflection); } p.translate(-offsetX, -offsetY); } _bubble.paintBubble( p, bubbleRect, _premiumPossible ? QBrush(_cachedGradient) : st::windowBgActive->b); } class Line final : public Ui::RpWidget { public: Line( not_null parent, const style::PremiumLimits &st, int max, TextFactory textFactory, int min, float64 ratio); Line( not_null parent, const style::PremiumLimits &st, QString max, QString min, float64 ratio); Line( not_null parent, const style::PremiumLimits &st, LimitRowLabels labels, rpl::producer state); void setColorOverride(QBrush brush); protected: void paintEvent(QPaintEvent *event) override; private: void recache(const QSize &s); const style::PremiumLimits &_st; QPixmap _leftPixmap; QPixmap _rightPixmap; float64 _ratio = 0.; Ui::Animations::Simple _animation; rpl::event_stream<> _recaches; Ui::Text::String _leftLabel; Ui::Text::String _leftText; Ui::Text::String _rightLabel; Ui::Text::String _rightText; bool _dynamic = false; std::optional _overrideBrush; }; Line::Line( not_null parent, const style::PremiumLimits &st, int max, TextFactory textFactory, int min, float64 ratio) : Line( parent, st, max ? textFactory(max) : QString(), min ? textFactory(min) : QString(), ratio) { } Line::Line( not_null parent, const style::PremiumLimits &st, QString max, QString min, float64 ratio) : Line(parent, st, LimitRowLabels{ .leftLabel = tr::lng_premium_free(), .leftCount = rpl::single(min), .rightLabel = tr::lng_premium(), .rightCount = rpl::single(max), }, rpl::single(LimitRowState{ ratio })) { } Line::Line( not_null parent, const style::PremiumLimits &st, LimitRowLabels labels, rpl::producer state) : Ui::RpWidget(parent) , _st(st) { resize(width(), st::requestsAcceptButton.height); const auto set = [&]( Ui::Text::String &label, rpl::producer &text) { std::move(text) | rpl::start_with_next([=, &label](QString text) { label = { st::semiboldTextStyle, text }; _recaches.fire({}); }, lifetime()); }; set(_leftLabel, labels.leftLabel); set(_leftText, labels.leftCount); set(_rightLabel, labels.rightLabel); set(_rightText, labels.rightCount); std::move(state) | rpl::start_with_next([=](LimitRowState state) { _dynamic = state.dynamic; if (width() > 0) { const auto from = state.animateFromZero ? 0. : _animation.value(_ratio); const auto duration = kSlideDuration * kStepBeforeDeflection; _animation.start([=] { update(); }, from, state.ratio, duration, anim::easeOutCirc); } _ratio = state.ratio; }, lifetime()); rpl::combine( sizeValue(), parent->widthValue(), _recaches.events_starting_with({}) ) | rpl::filter([](const QSize &size, int parentWidth, auto) { return !size.isEmpty() && parentWidth; }) | rpl::start_with_next([=](const QSize &size, auto, auto) { recache(size); update(); }, lifetime()); } void Line::setColorOverride(QBrush brush) { if (brush.style() == Qt::NoBrush) { _overrideBrush = std::nullopt; } else { _overrideBrush = brush; } } void Line::paintEvent(QPaintEvent *event) { Painter p(this); const auto ratio = _animation.value(_ratio); const auto left = int(base::SafeRound(ratio * width())); const auto dpr = int(_leftPixmap.devicePixelRatio()); const auto height = _leftPixmap.height() / dpr; p.drawPixmap( QRect(0, 0, left, height), _leftPixmap, QRect(0, 0, left * dpr, height * dpr)); p.drawPixmap( QRect(left, 0, width() - left, height), _rightPixmap, QRect(left * dpr, 0, (width() - left) * dpr, height * dpr)); p.setFont(st::normalFont); const auto textPadding = st::premiumLineTextSkip; const auto textTop = (height - _leftLabel.minHeight()) / 2; const auto leftMinWidth = _leftLabel.maxWidth() + _leftText.maxWidth() + 3 * textPadding; const auto pen = [&](bool gradient) { return gradient ? st::activeButtonFg : _st.nonPremiumFg; }; if (!_dynamic && left >= leftMinWidth) { p.setPen(pen(_st.gradientFromLeft)); _leftLabel.drawLeft( p, textPadding, textTop, left - textPadding, left); _leftText.drawRight( p, textPadding, textTop, left - textPadding, left, style::al_right); } const auto right = width() - left; const auto rightMinWidth = 2 * _rightText.maxWidth() + 3 * textPadding; if (!_dynamic && right >= rightMinWidth) { p.setPen(pen(!_st.gradientFromLeft)); _rightLabel.drawLeftElided( p, left + textPadding, textTop, (right - _rightText.countWidth(right) - textPadding * 2), right); _rightText.drawRight( p, textPadding, textTop, right - textPadding, width(), style::al_right); } } void Line::recache(const QSize &s) { const auto r = [&](int width) { return QRect(0, 0, width, s.height()); }; const auto pixmap = [&](int width) { auto result = QPixmap(r(width).size() * style::DevicePixelRatio()); result.setDevicePixelRatio(style::DevicePixelRatio()); result.fill(Qt::transparent); return result; }; const auto pathRound = [&](int width) { auto result = QPainterPath(); result.addRoundedRect( r(width), st::premiumLineRadius, st::premiumLineRadius); return result; }; const auto width = s.width(); const auto fill = [&](QPainter &p, QPainterPath path, bool gradient) { if (!gradient) { p.fillPath(path, _st.nonPremiumBg); } else if (_overrideBrush) { p.fillPath(path, *_overrideBrush); } else { p.fillPath(path, QBrush(ComputeGradient(this, 0, width))); } }; const auto textPadding = st::premiumLineTextSkip; const auto textTop = (s.height() - _leftLabel.minHeight()) / 2; const auto rwidth = _rightLabel.maxWidth(); const auto pen = [&](bool gradient) { return gradient ? st::activeButtonFg : _st.nonPremiumFg; }; { auto leftPixmap = pixmap(width); auto p = Painter(&leftPixmap); auto hq = PainterHighQualityEnabler(p); fill(p, pathRound(width), _st.gradientFromLeft); if (_dynamic) { p.setFont(st::normalFont); p.setPen(pen(_st.gradientFromLeft)); _leftLabel.drawLeft(p, textPadding, textTop, width, width); _rightLabel.drawRight(p, textPadding, textTop, rwidth, width); } _leftPixmap = std::move(leftPixmap); } { auto rightPixmap = pixmap(width); auto p = Painter(&rightPixmap); auto hq = PainterHighQualityEnabler(p); fill(p, pathRound(width), !_st.gradientFromLeft); if (_dynamic) { p.setFont(st::normalFont); p.setPen(pen(!_st.gradientFromLeft)); _leftLabel.drawLeft(p, textPadding, textTop, width, width); _rightLabel.drawRight(p, textPadding, textTop, rwidth, width); } _rightPixmap = std::move(rightPixmap); } } } // namespace QString Svg() { return u":/gui/icons/settings/star.svg"_q; } QByteArray ColorizedSvg(const QGradientStops &gradientStops) { auto f = QFile(Svg()); if (!f.open(QIODevice::ReadOnly)) { return QByteArray(); } auto content = QString::fromUtf8(f.readAll()); auto stops = [&] { auto s = QString(); for (const auto &stop : gradientStops) { s += QString("") .arg(QString::number(stop.first), stop.second.name()); } return s; }(); const auto color = QString("%5") .arg(0) .arg(1) .arg(1) .arg(0) .arg(std::move(stops)); content.replace(u"gradientPlaceholder"_q, color); content.replace(u"#fff"_q, u"url(#Gradient2)"_q); f.close(); return content.toUtf8(); } QImage GenerateStarForLightTopBar(QRectF rect) { auto svg = QSvgRenderer(Ui::Premium::Svg()); const auto size = rect.size().toSize(); auto frame = QImage( size * style::DevicePixelRatio(), QImage::Format_ARGB32_Premultiplied); frame.setDevicePixelRatio(style::DevicePixelRatio()); auto mask = frame; mask.fill(Qt::transparent); { auto p = QPainter(&mask); auto gradient = QLinearGradient( 0, size.height(), size.width(), 0); gradient.setStops(Ui::Premium::ButtonGradientStops()); p.setPen(Qt::NoPen); p.setBrush(gradient); p.drawRect(0, 0, size.width(), size.height()); } frame.fill(Qt::transparent); { auto q = QPainter(&frame); svg.render(&q, QRect(QPoint(), size)); q.setCompositionMode(QPainter::CompositionMode_SourceIn); q.drawImage(0, 0, mask); } return frame; } void AddBubbleRow( not_null parent, const style::PremiumBubble &st, rpl::producer<> showFinishes, int min, int current, int max, bool premiumPossible, std::optional> phrase, const style::icon *icon) { AddBubbleRow( parent, st, std::move(showFinishes), rpl::single(BubbleRowState{ .counter = current, .ratio = (current - min) / float64(max - min), }), premiumPossible, ProcessTextFactory(phrase), icon, st::boxRowPadding); } void AddBubbleRow( not_null parent, const style::PremiumBubble &st, rpl::producer<> showFinishes, rpl::producer state, bool premiumPossible, Fn text, const style::icon *icon, const style::margins &outerPadding) { const auto container = parent->add( object_ptr(parent, 0)); const auto bubble = Ui::CreateChild( container, st, text ? std::move(text) : ProcessTextFactory(std::nullopt), std::move(state), premiumPossible, std::move(showFinishes), icon, outerPadding); rpl::combine( container->sizeValue(), bubble->sizeValue() ) | rpl::start_with_next([=](const QSize &parentSize, const QSize &size) { container->resize(parentSize.width(), size.height()); }, bubble->lifetime()); bubble->show(); } void AddLimitRow( not_null parent, const style::PremiumLimits &st, QString max, QString min, float64 ratio) { parent->add( object_ptr(parent, st, max, min, ratio), st::boxRowPadding); } void AddLimitRow( not_null parent, const style::PremiumLimits &st, int max, std::optional> phrase, int min, float64 ratio) { const auto factory = ProcessTextFactory(phrase); AddLimitRow( parent, st, max ? factory(max) : QString(), min ? factory(min) : QString(), ratio); } void AddLimitRow( not_null parent, const style::PremiumLimits &st, LimitRowLabels labels, rpl::producer state, const style::margins &padding) { parent->add( object_ptr(parent, st, std::move(labels), std::move(state)), padding); } void AddAccountsRow( not_null parent, AccountsRowArgs &&args) { const auto container = parent->add( object_ptr(parent, st::premiumAccountsHeight), st::boxRowPadding); struct Account { not_null widget; Ui::RoundImageCheckbox checkbox; Ui::Text::String name; QPixmap badge; }; struct State { std::vector accounts; }; const auto state = container->lifetime().make_state(); const auto group = args.group; const auto imageRadius = args.st.imageRadius; const auto checkSelectWidth = args.st.selectWidth; const auto nameFg = args.stNameFg; const auto cacheBadge = [=](int center) { const auto &padding = st::premiumAccountsLabelPadding; const auto size = st::premiumAccountsLabelSize + QSize( padding.left() + padding.right(), padding.top() + padding.bottom()); auto badge = QPixmap(size * style::DevicePixelRatio()); badge.setDevicePixelRatio(style::DevicePixelRatio()); badge.fill(Qt::transparent); auto p = QPainter(&badge); auto hq = PainterHighQualityEnabler(p); p.setPen(Qt::NoPen); const auto rectOut = QRect(QPoint(), size); const auto rectIn = rectOut - padding; const auto radius = st::premiumAccountsLabelRadius; p.setBrush(st::premiumButtonFg); p.drawRoundedRect(rectOut, radius, radius); const auto left = center - rectIn.width() / 2; p.setBrush(QBrush(ComputeGradient(container, left, rectIn.width()))); p.drawRoundedRect(rectIn, radius / 2, radius / 2); p.setPen(st::premiumButtonFg); p.setFont(st::semiboldFont); p.drawText(rectIn, u"+1"_q, style::al_center); return badge; }; for (auto &entry : args.entries) { const auto widget = Ui::CreateChild(container); auto name = Ui::Text::String(imageRadius * 2); name.setText(args.stName, entry.name, Ui::NameTextOptions()); state->accounts.push_back(Account{ .widget = widget, .checkbox = Ui::RoundImageCheckbox( args.st, [=] { widget->update(); }, base::take(entry.paintRoundImage)), .name = std::move(name), }); const auto index = int(state->accounts.size()) - 1; state->accounts[index].checkbox.setChecked( index == group->current(), anim::type::instant); widget->paintRequest( ) | rpl::start_with_next([=] { Painter p(widget); const auto width = widget->width(); const auto photoLeft = (width - (imageRadius * 2)) / 2; const auto photoTop = checkSelectWidth; const auto &account = state->accounts[index]; account.checkbox.paint(p, photoLeft, photoTop, width); const auto &badgeSize = account.badge.size() / style::DevicePixelRatio(); p.drawPixmap( (width - badgeSize.width()) / 2, photoTop + (imageRadius * 2) - badgeSize.height() / 2, account.badge); p.setPen(nameFg); p.setBrush(Qt::NoBrush); account.name.drawLeftElided( p, 0, photoTop + imageRadius * 2 + st::premiumAccountsNameTop, width, width, 2, style::al_top, 0, -1, 0, true); }, widget->lifetime()); widget->setClickedCallback([=] { group->setValue(index); }); } container->sizeValue( ) | rpl::start_with_next([=](const QSize &size) { const auto count = state->accounts.size(); const auto columnWidth = size.width() / count; for (auto i = 0; i < count; i++) { auto &account = state->accounts[i]; account.widget->resize(columnWidth, size.height()); const auto left = columnWidth * i; account.widget->moveToLeft(left, 0); account.badge = cacheBadge(left + columnWidth / 2); const auto photoWidth = ((imageRadius + checkSelectWidth) * 2); account.checkbox.setColorOverride(QBrush( ComputeGradient( container, left + (columnWidth - photoWidth) / 2, photoWidth))); } }, container->lifetime()); group->setChangedCallback([=](int value) { for (auto i = 0; i < state->accounts.size(); i++) { state->accounts[i].checkbox.setChecked(i == value); } }); } QGradientStops LimitGradientStops() { return { { 0.0, st::premiumButtonBg1->c }, { .25, st::premiumButtonBg1->c }, { .85, st::premiumButtonBg2->c }, { 1.0, st::premiumButtonBg3->c }, }; } QGradientStops ButtonGradientStops() { return { { 0., st::premiumButtonBg1->c }, { 0.6, st::premiumButtonBg2->c }, { 1., st::premiumButtonBg3->c }, }; } QGradientStops LockGradientStops() { return ButtonGradientStops(); } QGradientStops FullHeightGradientStops() { return { { 0.0, st::premiumIconBg1->c }, { .28, st::premiumIconBg2->c }, { .55, st::premiumButtonBg2->c }, { .75, st::premiumButtonBg1->c }, { 1.0, st::premiumIconBg3->c }, }; } QGradientStops GiftGradientStops() { return { { 0., st::premiumButtonBg1->c }, { 1., st::premiumButtonBg2->c }, }; } QGradientStops StoriesIconsGradientStops() { return { { 0., st::premiumButtonBg1->c }, { .33, st::premiumButtonBg2->c }, { .66, st::premiumButtonBg3->c }, { 1., st::premiumIconBg1->c }, }; } QGradientStops CreditsIconGradientStops() { return { { 0., st::creditsBg1->c }, { 1., st::creditsBg2->c }, }; } void ShowListBox( not_null box, const style::PremiumLimits &st, std::vector entries) { box->setWidth(st::boxWideWidth); const auto &stLabel = st::defaultFlatLabel; const auto &titlePadding = st::settingsPremiumPreviewTitlePadding; const auto &aboutPadding = st::settingsPremiumPreviewAboutPadding; const auto iconTitlePadding = st::settingsPremiumPreviewIconTitlePadding; const auto iconAboutPadding = st::settingsPremiumPreviewIconAboutPadding; auto lines = std::vector(); lines.reserve(int(entries.size())); auto icons = std::shared_ptr>(); const auto content = box->verticalLayout(); for (auto &entry : entries) { const auto title = content->add( object_ptr( content, base::take(entry.title) | Ui::Text::ToBold(), stLabel), entry.icon ? iconTitlePadding : titlePadding); content->add( object_ptr( content, base::take(entry.about), st::boxDividerLabel), entry.icon ? iconAboutPadding : aboutPadding); if (const auto outlined = entry.icon) { if (!icons) { icons = std::make_shared>(); } const auto index = int(icons->size()); icons->push_back(QColor()); const auto icon = Ui::CreateChild(content.get()); icon->resize(outlined->size()); title->topValue() | rpl::start_with_next([=](int y) { const auto shift = st::settingsPremiumPreviewIconPosition; icon->move(QPoint(0, y) + shift); }, icon->lifetime()); icon->paintRequest() | rpl::start_with_next([=] { auto p = QPainter(icon); outlined->paintInCenter(p, icon->rect(), (*icons)[index]); }, icon->lifetime()); } if (entry.leftNumber || entry.rightNumber) { auto factory = [=, text = ProcessTextFactory({})](int n) { if (entry.customRightText && (n == entry.rightNumber)) { return *entry.customRightText; } else { return text(n); } }; const auto limitRow = content->add( object_ptr( content, st, entry.rightNumber, TextFactory(std::move(factory)), entry.leftNumber, kLimitRowRatio), st::settingsPremiumPreviewLinePadding); lines.push_back(limitRow); } } content->resizeToWidth(content->height()); // Colors for icons. if (icons) { box->addSkip(st::settingsPremiumPreviewLinePadding.bottom()); const auto stops = Ui::Premium::StoriesIconsGradientStops(); for (auto i = 0, count = int(icons->size()); i != count; ++i) { (*icons)[i] = anim::gradient_color_at( stops, (count > 1) ? (i / float64(count - 1)) : 0.); } } // Color lines. if (!lines.empty()) { box->addSkip(st::settingsPremiumPreviewLinePadding.bottom()); const auto from = lines.front()->y(); const auto to = lines.back()->y() + lines.back()->height(); const auto partialGradient = [&] { auto stops = Ui::Premium::FullHeightGradientStops(); // Reverse. for (auto &stop : stops) { stop.first = std::abs(stop.first - 1.); } return PartialGradient(from, to, std::move(stops)); }(); for (auto i = 0; i < int(lines.size()); i++) { const auto &line = lines[i]; const auto brush = QBrush( partialGradient.compute(line->y(), line->height())); line->setColorOverride(brush); } box->addSkip(st::settingsPremiumPreviewLinePadding.bottom()); } } void AddGiftOptions( not_null parent, std::shared_ptr group, std::vector gifts, const style::PremiumOption &st, bool topBadges) { struct Edges { Ui::RpWidget *top = nullptr; Ui::RpWidget *bottom = nullptr; }; const auto edges = parent->lifetime().make_state(); struct Animation { int nowIndex = 0; Ui::Animations::Simple animation; }; const auto wasGroupValue = group->current(); const auto animation = parent->lifetime().make_state(); animation->nowIndex = wasGroupValue; const auto stops = GiftGradientStops(); const auto addRow = [&](const Data::SubscriptionOption &info, int index) { const auto row = parent->add( object_ptr(parent), st.rowPadding); row->resize(row->width(), st.rowHeight); { if (!index) { edges->top = row; } edges->bottom = row; } const auto &stCheckbox = st::defaultBoxCheckbox; auto radioView = std::make_unique( st::defaultRadio, (group->hasValue() && group->current() == index)); const auto radioViewRaw = radioView.get(); const auto radio = Ui::CreateChild( row, group, index, QString(), stCheckbox, std::move(radioView)); radio->setAttribute(Qt::WA_TransparentForMouseEvents); radio->show(); { // Paint the last frame instantly for the layer animation. group->setValue(0); group->setValue(wasGroupValue); radio->finishAnimating(); } row->sizeValue( ) | rpl::start_with_next([=, margins = stCheckbox.margin]( const QSize &s) { const auto radioHeight = radio->height() - margins.top() - margins.bottom(); radio->moveToLeft( st.rowMargins.left(), (s.height() - radioHeight) / 2); }, radio->lifetime()); { auto onceLifetime = std::make_shared(); row->paintRequest( ) | rpl::take( 1 ) | rpl::start_with_next([=]() mutable { const auto from = edges->top->y(); const auto to = edges->bottom->y() + edges->bottom->height(); auto partialGradient = PartialGradient(from, to, stops); radioViewRaw->setBrush( partialGradient.compute(row->y(), row->height())); if (onceLifetime) { base::take(onceLifetime)->destroy(); } }, *onceLifetime); } row->paintRequest( ) | rpl::start_with_next([=](const QRect &r) { auto p = QPainter(row); auto hq = PainterHighQualityEnabler(p); p.fillRect(r, Qt::transparent); const auto left = st.textLeft; const auto halfHeight = row->height() / 2; const auto titleFont = st::semiboldFont; p.setFont(titleFont); p.setPen(st::boxTextFg); if (info.costPerMonth.isEmpty() && info.discount.isEmpty()) { const auto r = row->rect().translated( -row->rect().left() + left, 0); p.drawText(r, info.duration, style::al_left); } else { p.drawText( left, st.subtitleTop + titleFont->ascent, info.duration); } const auto discountFont = st::windowFiltersButton.badgeStyle.font; const auto discountWidth = discountFont->width(info.discount); const auto &discountMargins = discountWidth ? st.badgeMargins : style::margins(); const auto bottomLeftRect = QRect( left, halfHeight + discountMargins.top(), discountWidth + discountMargins.left() + discountMargins.right(), st.badgeHeight); const auto discountRect = topBadges ? bottomLeftRect.translated( titleFont->width(info.duration) + st.badgeShift.x(), -bottomLeftRect.top() + st.badgeShift.y() + st.subtitleTop + (titleFont->height - bottomLeftRect.height()) / 2) : bottomLeftRect; const auto from = edges->top->y(); const auto to = edges->bottom->y() + edges->bottom->height(); auto partialGradient = PartialGradient(from, to, stops); const auto partialGradientBrush = partialGradient.compute( row->y(), row->height()); { p.setPen(Qt::NoPen); p.setBrush(partialGradientBrush); const auto round = st.badgeRadius; p.drawRoundedRect(discountRect, round, round); } if (st.borderWidth && (animation->nowIndex == index)) { const auto progress = animation->animation.value(1.); const auto w = row->width(); auto gradient = QLinearGradient(w - w * progress, 0, w * 2, 0); gradient.setSpread(QGradient::Spread::RepeatSpread); gradient.setStops(stops); const auto pen = QPen( QBrush(partialGradientBrush), st.borderWidth); p.setPen(pen); p.setBrush(Qt::NoBrush); const auto borderRect = row->rect() - Margins(pen.width() / 2); const auto round = st.borderRadius; p.drawRoundedRect(borderRect, round, round); } p.setPen(st::premiumButtonFg); p.setFont(discountFont); p.drawText(discountRect, info.discount, style::al_center); const auto perRect = QMargins(0, 0, row->width(), 0) + bottomLeftRect.translated( topBadges ? 0 : bottomLeftRect.width() + discountMargins.left(), 0); p.setPen(st::windowSubTextFg); p.setFont(st::shareBoxListItem.nameStyle.font); p.drawText(perRect, info.costPerMonth, style::al_left); const auto totalRect = row->rect() - QMargins(0, 0, st.rowMargins.right(), 0); p.setFont(st::normalFont); p.drawText(totalRect, info.costTotal, style::al_right); }, row->lifetime()); row->setClickedCallback([=, duration = st::defaultCheck.duration] { group->setValue(index); animation->nowIndex = group->current(); animation->animation.stop(); animation->animation.start( [=] { parent->update(); }, 0., 1., duration); }); }; for (auto i = 0; i < gifts.size(); i++) { addRow(gifts[i], i); } parent->resizeToWidth(parent->height()); parent->update(); } } // namespace Premium } // namespace Ui