diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index 8c1456e842..f45df83054 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -1077,3 +1077,10 @@ ringtonesBoxButton: SettingsButton(defaultSettingsButton) { iconLeft: 25px; } ringtonesBoxSkip: 7px; + +premiumBubblePadding: margins(14px, 0px, 14px, 0px); +premiumBubbleHeight: 40px; +premiumBubbleSkip: 5px; +premiumBubbleSlideDuration: 500; +premiumBubbleTailSize: size(21px, 7px); +premiumBubbleFont: font(19px); diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.cpp b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp new file mode 100644 index 0000000000..fe53d8bb6d --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.cpp @@ -0,0 +1,389 @@ +/* +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 "lang/lang_keys.h" +#include "ui/effects/animations.h" +#include "ui/effects/gradient.h" +#include "ui/effects/numbers_animation.h" +#include "ui/wrap/padding_wrap.h" +#include "ui/wrap/vertical_layout.h" +#include "styles/style_boxes.h" +#include "styles/style_widgets.h" + +namespace Ui { +namespace Premium { +namespace { + +using TextFactory = Fn; + +constexpr auto kDeflection = 30.; + +constexpr auto kStepBeforeDeflection = 0.75; +constexpr auto kStepAfterDeflection = kStepBeforeDeflection + + (1. - kStepBeforeDeflection) / 2.; + +[[nodiscard]] QGradientStops GradientStops() { + return QGradientStops{ + QGradientStop(0.0, st::premiumButtonBg1->c), + QGradientStop(.25, st::premiumButtonBg1->c), + QGradientStop(.85, st::premiumButtonBg2->c), + QGradientStop(1.0, st::premiumButtonBg3->c), + }; +} + +[[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(GradientStops()); + + 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 Bubble final { +public: + Bubble( + Fn updateCallback, + TextFactory textFactory, + const style::icon *icon); + + [[nodiscard]] int counter() const; + [[nodiscard]] int height() const; + [[nodiscard]] int width() const; + [[nodiscard]] int bubbleRadius() const; + + void setCounter(int value); + void setTailEdge(std::optional edge); + void paintBubble(Painter &p, const QRect &r, const QBrush &brush); + + [[nodiscard]] rpl::producer<> widthChanges() const; + [[nodiscard]] rpl::producer<> updateRequests() const; + +private: + const Fn _updateCallback; + const TextFactory _textFactory; + + const style::font &_font; + const style::margins &_padding; + const style::icon *_icon; + NumbersAnimation _numberAnimation; + const QSize _tailSize; + const int _height; + const int _textTop; + + int _counter = -1; + std::optional _tailEdge; + + rpl::event_stream<> _widthChanges; + +}; + +Bubble::Bubble( + Fn updateCallback, + TextFactory textFactory, + const style::icon *icon) +: _updateCallback(std::move(updateCallback)) +, _textFactory(std::move(textFactory)) +, _font(st::premiumBubbleFont) +, _padding(st::premiumBubblePadding) +, _icon(icon) +, _numberAnimation(_font, _updateCallback) +, _tailSize(st::premiumBubbleTailSize) +, _height(st::premiumBubbleHeight + _tailSize.height()) +, _textTop((_height - _tailSize.height() - _font->height) / 2) { + _numberAnimation.setWidthChangedCallback([=] { + _widthChanges.fire({}); + }); +} + +int Bubble::counter() const { + return _counter; +} + +int Bubble::height() const { + return _height; +} + +int Bubble::bubbleRadius() const { + return (_height - _tailSize.height()) / 2; +} + +int Bubble::width() const { + return _padding.left() + + _icon->width() + + st::premiumBubbleTextSkip + + _numberAnimation.countWidth() + + _padding.right(); +} + +void Bubble::setCounter(int value) { + if (_counter != value) { + _counter = value; + _numberAnimation.setText(_textFactory(_counter), _counter); + } +} + +void Bubble::setTailEdge(std::optional edge) { + _tailEdge = edge; +} + +void Bubble::paintBubble(Painter &p, const QRect &r, const QBrush &brush) { + if (_counter < 0) { + return; + } + + const auto bubbleRect = r - style::margins{ 0, 0, 0, _tailSize.height() }; + { + PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + p.setBrush(brush); + const auto radius = bubbleRadius(); + auto pathTail = QPainterPath(); + + const auto offset = bubbleRect.topLeft() + + QPoint( + (_tailEdge.value_or(Qt::TopEdge) == Qt::RightEdge) + ? (bubbleRect.width() - _tailSize.width() - radius) + : (bubbleRect.width() - _tailSize.width()) / 2, + bubbleRect.height()) + - QPoint(0, 1); + + pathTail.moveTo(offset); + pathTail.lineTo(QPoint(_tailSize.width() / 2, _tailSize.height()) + + offset); + pathTail.lineTo(offset + QPoint(_tailSize.width(), 0)); + pathTail.lineTo(offset); + + auto pathBubble = QPainterPath(); + pathBubble.setFillRule(Qt::WindingFill); + pathBubble.addRoundedRect(bubbleRect, radius, radius); + + p.fillPath(pathTail + pathBubble, p.brush()); + } + p.setPen(st::activeButtonFg); + p.setFont(_font); + const auto iconLeft = r.x() + _padding.left(); + _icon->paint( + p, + iconLeft, + r.y() + (bubbleRect.height() - _icon->height()) / 2, + r.width()); + _numberAnimation.paint( + p, + iconLeft + _icon->width() + st::premiumBubbleTextSkip, + r.y() + _textTop, + width() / 2); +} + +rpl::producer<> Bubble::widthChanges() const { + return _widthChanges.events(); +} + +class BubbleWidget final : public Ui::RpWidget { +public: + BubbleWidget( + not_null parent, + TextFactory textFactory, + int current, + int maxCounter, + rpl::producer<> showFinishes, + const style::icon *icon); + + [[nodiscard]] bool animating() const; + +protected: + void paintEvent(QPaintEvent *e) override; + +private: + const int _currentCounter; + const int _maxCounter; + Bubble _bubble; + + Ui::Animations::Simple _appearanceAnimation; + QSize _spaceForDeflection; + + QLinearGradient _cachedGradient; + +}; + +BubbleWidget::BubbleWidget( + not_null parent, + TextFactory textFactory, + int current, + int maxCounter, + rpl::producer<> showFinishes, + const style::icon *icon) +: RpWidget(parent) +, _currentCounter(current) +, _maxCounter(maxCounter) +, _bubble([=] { update(); }, std::move(textFactory), icon) { + const auto resizeTo = [=](int w, int h) { + _spaceForDeflection = QSize( + st::premiumBubbleSkip, + st::premiumBubbleSkip); + resize(QSize(w, h) + _spaceForDeflection); + }; + + resizeTo(_bubble.width(), _bubble.height()); + _bubble.widthChanges( + ) | rpl::start_with_next([=] { + resizeTo(_bubble.width(), _bubble.height()); + }, lifetime()); + + const auto moveEndPoint = _currentCounter / float64(_maxCounter); + const auto computeLeft = [=](float64 pointRatio, float64 animProgress) { + const auto &padding = st::boxRowPadding; + const auto left = _bubble.bubbleRadius() + padding.left(); + const auto right = _bubble.bubbleRadius() + padding.right(); + return ((parent->width() - left - right) + * pointRatio + * animProgress) + - (_bubble.width() / 2) + + left; + }; + + std::move( + showFinishes + ) | rpl::take(1) | rpl::start_with_next([=] { + const auto computeEdge = [=] { + return computeLeft(1., 1.); + }; + const auto checkBubbleEdges = [&]() -> std::optional { + const auto finish = computeLeft(moveEndPoint, 1.); + if (finish >= computeEdge()) { + return Qt::RightEdge; + } + return std::nullopt; + }; + _bubble.setTailEdge(checkBubbleEdges()); + + _appearanceAnimation.start([=](float64 value) { + const auto moveProgress = std::clamp( + (value / kStepBeforeDeflection), + 0., + 1.); + const auto counterProgress = std::clamp( + (value / kStepAfterDeflection), + 0., + 1.); + moveToLeft( + std::clamp( + int(computeLeft(moveEndPoint, moveProgress)), + 0, + int(computeEdge())), + 0); + + const auto counter = int(0 + counterProgress * _currentCounter); + if (!(counter % 4) || counterProgress > 0.8) { + _bubble.setCounter(counter); + } + update(); + }, + 0., + 1., + st::premiumBubbleSlideDuration, + anim::easeOutCubic); + }, lifetime()); +} + +bool BubbleWidget::animating() const { + return _appearanceAnimation.animating(); +} + +void BubbleWidget::paintEvent(QPaintEvent *e) { + Painter p(this); + + const auto padding = QMargins( + 0, + _spaceForDeflection.height(), + _spaceForDeflection.width(), + 0); + const auto bubbleRect = rect() - padding; + + if (_appearanceAnimation.animating()) { + auto gradient = ComputeGradient( + parentWidget(), + x(), + bubbleRect.width()); + _cachedGradient = std::move(gradient); + + const auto progress = _appearanceAnimation.value(1.); + const auto scaleProgress = std::clamp( + (progress / kStepBeforeDeflection), + 0., + 1.); + const auto scale = scaleProgress; + const auto rotationProgress = std::clamp( + (progress - kStepBeforeDeflection) / (1. - kStepBeforeDeflection), + 0., + 1.); + const auto rotationProgressReverse = std::clamp( + (progress - kStepAfterDeflection) / (1. - kStepAfterDeflection), + 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); + p.rotate(rotationProgress * kDeflection + - rotationProgressReverse * kDeflection); + p.translate(-offsetX, -offsetY); + } + + _bubble.paintBubble(p, bubbleRect, QBrush(_cachedGradient)); +} + +} // namespace + +void AddBubbleRow( + not_null parent, + rpl::producer<> showFinishes, + int min, + int current, + int max, + std::optional> phrase, + const style::icon *icon) { + auto textFactory = phrase + ? TextFactory([=](int n) { return (*phrase)(tr::now, lt_count, n); }) + : TextFactory([=](int n) { return QString::number(n); }); + + const auto container = parent->add( + object_ptr(parent, 0)); + const auto bubble = Ui::CreateChild( + container, + std::move(textFactory), + current, + max, + std::move(showFinishes), + icon); + rpl::combine( + container->sizeValue(), + bubble->sizeValue() + ) | rpl::start_with_next([=](const QSize &parentSize, const QSize &size) { + container->resize(parentSize.width(), size.height()); + }, bubble->lifetime()); +} + +} // namespace Premium +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/effects/premium_graphics.h b/Telegram/SourceFiles/ui/effects/premium_graphics.h new file mode 100644 index 0000000000..468a565616 --- /dev/null +++ b/Telegram/SourceFiles/ui/effects/premium_graphics.h @@ -0,0 +1,33 @@ +/* +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 + +namespace tr { +template +struct phrase; +} // namespace tr + +enum lngtag_count : int; + +namespace Ui { + +class VerticalLayout; + +namespace Premium { + +void AddBubbleRow( + not_null parent, + rpl::producer<> showFinishes, + int min, + int current, + int max, + std::optional> phrase, + const style::icon *icon); + +} // namespace Premium +} // namespace Ui diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 4dacf7817d..d4a4680c99 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -228,6 +228,8 @@ PRIVATE ui/controls/who_reacted_context_action.h ui/controls/window_outdated_bar.cpp ui/controls/window_outdated_bar.h + ui/effects/premium_graphics.cpp + ui/effects/premium_graphics.h ui/effects/scroll_content_shadow.cpp ui/effects/scroll_content_shadow.h ui/text/format_song_name.cpp