From 07fd9b30744c96d0da892183bbec379267465022 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 16 Jan 2025 22:16:08 +0400 Subject: [PATCH] Show nice collectible tooltip on wearing. --- Telegram/SourceFiles/info/info.style | 6 + Telegram/SourceFiles/info/info_controller.cpp | 4 + Telegram/SourceFiles/info/info_controller.h | 7 +- .../info/profile/info_profile_actions.cpp | 3 +- .../info/profile/info_profile_cover.cpp | 331 +++++++++++++++++- .../info/profile/info_profile_cover.h | 18 +- 6 files changed, 357 insertions(+), 12 deletions(-) diff --git a/Telegram/SourceFiles/info/info.style b/Telegram/SourceFiles/info/info.style index 2483ae755..9709579e9 100644 --- a/Telegram/SourceFiles/info/info.style +++ b/Telegram/SourceFiles/info/info.style @@ -1170,3 +1170,9 @@ infoSharedMediaScroll: ScrollArea(defaultScrollArea) { width: 5px; deltax: 2px; } + +infoGiftTooltip: ImportantTooltip(defaultImportantTooltip) { + margin: margins(4px, 4px, 4px, 4px); + padding: margins(8px, 2px, 8px, 3px); +} +infoGiftTooltipFont: font(11px semibold); diff --git a/Telegram/SourceFiles/info/info_controller.cpp b/Telegram/SourceFiles/info/info_controller.cpp index cea7513f9..dc51c31e1 100644 --- a/Telegram/SourceFiles/info/info_controller.cpp +++ b/Telegram/SourceFiles/info/info_controller.cpp @@ -338,6 +338,10 @@ rpl::producer Controller::wrapValue() const { return _widget->wrapValue(); } +not_null Controller::wrapWidget() const { + return _widget; +} + bool Controller::validateMementoPeer( not_null memento) const { return memento->peer() == peer() diff --git a/Telegram/SourceFiles/info/info_controller.h b/Telegram/SourceFiles/info/info_controller.h index e6e18d606..9815c3bc0 100644 --- a/Telegram/SourceFiles/info/info_controller.h +++ b/Telegram/SourceFiles/info/info_controller.h @@ -304,11 +304,12 @@ public: return _section; } - bool validateMementoPeer( + [[nodiscard]] bool validateMementoPeer( not_null memento) const; - Wrap wrap() const; - rpl::producer wrapValue() const; + [[nodiscard]] Wrap wrap() const; + [[nodiscard]] rpl::producer wrapValue() const; + [[nodiscard]] not_null wrapWidget() const; void setSection(not_null memento); Ui::SearchFieldController *searchFieldController() const { diff --git a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp index 9551f822e..ffa2472ca 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_actions.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_actions.cpp @@ -2753,7 +2753,8 @@ Cover *AddCover( : container->add(object_ptr( container, controller->parentController(), - peer)); + peer, + [=] { return controller->wrapWidget(); })); result->showSection( ) | rpl::start_with_next([=](Section section) { controller->showSection(topic diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp index 4a17f7233..aaf8a8268 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.cpp +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.cpp @@ -8,9 +8,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "info/profile/info_profile_cover.h" #include "api/api_user_privacy.h" +#include "base/timer_rpl.h" #include "data/data_peer_values.h" #include "data/data_channel.h" #include "data/data_chat.h" +#include "data/data_emoji_statuses.h" #include "data/data_peer.h" #include "data/data_user.h" #include "data/data_document.h" @@ -33,8 +35,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/labels.h" #include "ui/widgets/popup_menu.h" #include "ui/text/text_utilities.h" +#include "ui/ui_utility.h" +#include "ui/painter.h" #include "base/event_filter.h" #include "base/unixtime.h" +#include "window/window_controller.h" #include "window/window_session_controller.h" #include "main/main_session.h" #include "settings/settings_premium.h" @@ -49,6 +54,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Info::Profile { namespace { +constexpr auto kWaitBeforeGiftBadge = crl::time(1000); +constexpr auto kGiftBadgeGlares = 3; +constexpr auto kGlareDurationStep = crl::time(320); +constexpr auto kGlareTimeout = crl::time(1000); + auto MembersStatusText(int count) { return tr::lng_chat_status_members(tr::now, lt_count_decimal, count); }; @@ -106,6 +116,240 @@ auto ChatStatusText(int fullCount, int onlineCount, bool isGroup) { } // namespace +class Cover::BadgeTooltip final : public Ui::RpWidget { +public: + BadgeTooltip( + not_null parent, + std::shared_ptr collectible, + not_null pointTo); + + void fade(bool shown); + void finishAnimating(); + + [[nodiscard]] crl::time glarePeriod() const; + +private: + void paintEvent(QPaintEvent *e) override; + + void setupGeometry(not_null pointTo); + void prepareImage(); + void showGlare(); + + const style::ImportantTooltip &_st; + std::shared_ptr _collectible; + QString _text; + const style::font &_font; + QSize _inner; + QSize _outer; + int _stroke = 0; + int _skip = 0; + QSize _full; + int _glareSize = 0; + int _glareRange = 0; + crl::time _glareDuration = 0; + base::Timer _glareTimer; + + Ui::Animations::Simple _showAnimation; + Ui::Animations::Simple _glareAnimation; + + QImage _image; + int _glareRight = 0; + int _imageGlareRight = 0; + int _arrowMiddle = 0; + int _imageArrowMiddle = 0; + + bool _shown = false; + +}; + +Cover::BadgeTooltip::BadgeTooltip( + not_null parent, + std::shared_ptr collectible, + not_null pointTo) +: Ui::RpWidget(parent) +, _st(st::infoGiftTooltip) +, _collectible(std::move(collectible)) +, _text(_collectible->title) +, _font(st::infoGiftTooltipFont) +, _inner(_font->width(_text), _font->height) +, _outer(_inner.grownBy(_st.padding)) +, _stroke(st::lineWidth) +, _skip(2 * _stroke) +, _full(_outer + QSize(2 * _skip, _st.arrow + 2 * _skip)) +, _glareSize(_outer.height() * 3) +, _glareRange(_outer.width() + _glareSize) +, _glareDuration(_glareRange * kGlareDurationStep / _glareSize) +, _glareTimer([=] { showGlare(); }) { + resize(_full + QSize(0, _st.shift)); + setupGeometry(pointTo); +} + +void Cover::BadgeTooltip::fade(bool shown) { + if (_shown == shown) { + return; + } + show(); + _shown = shown; + _showAnimation.start([=] { + update(); + if (!_showAnimation.animating()) { + if (!_shown) { + hide(); + } else { + showGlare(); + } + } + }, _shown ? 0. : 1., _shown ? 1. : 0., _st.duration, anim::easeInCirc); +} + +void Cover::BadgeTooltip::showGlare() { + _glareAnimation.start([=] { + update(); + if (!_glareAnimation.animating()) { + _glareTimer.callOnce(kGlareTimeout); + } + }, 0., 1., _glareDuration); +} + +void Cover::BadgeTooltip::finishAnimating() { + _showAnimation.stop(); + if (!_shown) { + hide(); + } +} + +crl::time Cover::BadgeTooltip::glarePeriod() const { + return _glareDuration + kGlareTimeout; +} + +void Cover::BadgeTooltip::paintEvent(QPaintEvent *e) { + const auto glare = _glareAnimation.value(0.); + _glareRight = anim::interpolate(0, _glareRange, glare); + prepareImage(); + + auto p = QPainter(this); + const auto shown = _showAnimation.value(_shown ? 1. : 0.); + p.setOpacity(shown); + const auto imageHeight = _image.height() / _image.devicePixelRatio(); + const auto top = anim::interpolate(0, height() - imageHeight, shown); + p.drawImage(0, top, _image); +} + +void Cover::BadgeTooltip::setupGeometry(not_null pointTo) { + auto widget = pointTo.get(); + const auto parent = parentWidget(); + + const auto refresh = [=] { + const auto rect = Ui::MapFrom(parent, pointTo, pointTo->rect()); + const auto point = QPoint(rect.center().x(), rect.y()); + const auto left = point.x() - (width() / 2); + const auto skip = _st.padding.left(); + setGeometry( + std::min(std::max(left, skip), parent->width() - width() - skip), + std::max(point.y() - height() - _st.margin.bottom(), skip), + width(), + height()); + const auto arrowMiddle = point.x() - x(); + if (_arrowMiddle != arrowMiddle) { + _arrowMiddle = arrowMiddle; + update(); + } + }; + refresh(); + while (widget && widget != parent) { + base::install_event_filter(this, widget, [=](not_null e) { + if (e->type() == QEvent::Resize || e->type() == QEvent::Move || e->type() == QEvent::ZOrderChange) { + refresh(); + raise(); + } + return base::EventFilterResult::Continue; + }); + widget = widget->parentWidget(); + } +} + +void Cover::BadgeTooltip::prepareImage() { + const auto ratio = style::DevicePixelRatio(); + const auto arrow = _st.arrow; + const auto size = _full * ratio; + if (_image.size() != size) { + _image = QImage(size, QImage::Format_ARGB32_Premultiplied); + _image.setDevicePixelRatio(ratio); + } else if (_imageGlareRight == _glareRight + && _imageArrowMiddle == _arrowMiddle) { + return; + } + _imageGlareRight = _glareRight; + _imageArrowMiddle = _arrowMiddle; + _image.fill(Qt::transparent); + + const auto gfrom = _imageGlareRight - _glareSize; + const auto gtill = _imageGlareRight; + + auto path = QPainterPath(); + const auto width = _outer.width(); + const auto height = _outer.height(); + const auto radius = (height + 1) / 2; + const auto diameter = height; + path.moveTo(radius, 0); + path.lineTo(width - radius, 0); + path.arcTo( + QRect(QPoint(width - diameter, 0), QSize(diameter, diameter)), + 90, + -180); + const auto xarrow = _arrowMiddle - _skip; + if (xarrow - arrow <= radius || xarrow + arrow >= width - radius) { + path.lineTo(radius, height); + } else { + path.lineTo(xarrow + arrow, height); + path.lineTo(xarrow, height + arrow); + path.lineTo(xarrow - arrow, height); + path.lineTo(radius, height); + } + path.arcTo( + QRect(QPoint(0, 0), QSize(diameter, diameter)), + -90, + -180); + path.closeSubpath(); + + auto p = QPainter(&_image); + auto hq = PainterHighQualityEnabler(p); + p.setPen(Qt::NoPen); + if (gtill > 0) { + auto gradient = QLinearGradient(gfrom, 0, gtill, 0); + gradient.setStops({ + { 0., _collectible->edgeColor }, + { 0.5, _collectible->centerColor }, + { 1., _collectible->edgeColor }, + }); + p.setBrush(gradient); + } else { + p.setBrush(_collectible->edgeColor); + } + p.translate(_skip, _skip); + p.drawPath(path); + p.setCompositionMode(QPainter::CompositionMode_Source); + p.setBrush(Qt::NoBrush); + auto copy = _collectible->textColor; + copy.setAlpha(0); + if (gtill > 0) { + auto gradient = QLinearGradient(gfrom, 0, gtill, 0); + gradient.setStops({ + { 0., copy }, + { 0.5, _collectible->textColor }, + { 1., copy }, + }); + p.setPen(QPen(gradient, _stroke)); + } else { + p.setPen(QPen(copy, _stroke)); + } + p.drawPath(path); + p.setCompositionMode(QPainter::CompositionMode_SourceOver); + p.setFont(_font); + p.setPen(QColor(255, 255, 255)); + p.drawText(_st.padding.left(), _st.padding.top() + _font->ascent, _text); +} + TopicIconView::TopicIconView( not_null topic, Fn paused, @@ -271,8 +515,16 @@ TopicIconButton::TopicIconButton( Cover::Cover( QWidget *parent, not_null controller, - not_null peer) -: Cover(parent, controller, peer, Role::Info, NameValue(peer)) { + not_null peer, + Fn()> parentForTooltip) +: Cover( + parent, + controller, + peer, + nullptr, + Role::Info, + NameValue(peer), + parentForTooltip) { } Cover::Cover( @@ -285,7 +537,8 @@ Cover::Cover( topic->channel(), topic, Role::Info, - TitleValue(topic)) { + TitleValue(topic), + nullptr) { } Cover::Cover( @@ -300,7 +553,8 @@ Cover::Cover( peer, nullptr, role, - std::move(title)) { + std::move(title), + nullptr) { } [[nodiscard]] rpl::producer BotVerifyBadgeForPeer( @@ -323,7 +577,8 @@ Cover::Cover( not_null peer, Data::ForumTopic *topic, Role role, - rpl::producer title) + rpl::producer title, + Fn()> parentForTooltip) : FixedHeightWidget(parent, CoverStyle(peer, topic, role).height) , _st(CoverStyle(peer, topic, role)) , _role(role) @@ -343,12 +598,13 @@ Cover::Cover( return controller->isGifPausedAtLeastFor( Window::GifPauseReason::Layer); })) +, _badgeContent(BadgeContentForPeer(peer)) , _badge( std::make_unique( this, st::infoPeerBadge, &peer->session(), - BadgeContentForPeer(peer), + _badgeContent.value(), _emojiStatusPanel.get(), [=] { return controller->isGifPausedAtLeastFor( @@ -365,6 +621,8 @@ Cover::Cover( return controller->isGifPausedAtLeastFor( Window::GifPauseReason::Layer); })) +, _parentForTooltip(std::move(parentForTooltip)) +, _badgeTooltipHide([=] { hideBadgeTooltip(); }) , _userpic(topic ? nullptr : object_ptr( @@ -416,6 +674,7 @@ Cover::Cover( initViewers(std::move(title)); setupChildGeometry(); + setupUniqueBadgeTooltip(); if (_userpic) { } else if (topic->canEdit()) { @@ -743,6 +1002,8 @@ void Cover::refreshStatusText() { } Cover::~Cover() { + base::take(_badgeTooltip); + base::take(_badgeOldTooltips); } void Cover::refreshNameGeometry(int newWidth) { @@ -791,4 +1052,62 @@ void Cover::refreshStatusGeometry(int newWidth) { newWidth); } +void Cover::hideBadgeTooltip() { + _badgeTooltipHide.cancel(); + if (auto old = base::take(_badgeTooltip)) { + const auto raw = old.get(); + _badgeOldTooltips.push_back(std::move(old)); + + raw->fade(false); + raw->shownValue( + ) | rpl::filter( + !rpl::mappers::_1 + ) | rpl::start_with_next([=] { + const auto i = ranges::find( + _badgeOldTooltips, + raw, + &std::unique_ptr::get); + if (i != end(_badgeOldTooltips)) { + _badgeOldTooltips.erase(i); + } + }, raw->lifetime()); + } +} + +void Cover::setupUniqueBadgeTooltip() { + base::timer_once(kWaitBeforeGiftBadge) | rpl::then( + _badge->updated() + ) | rpl::start_with_next([=] { + const auto widget = _badge->widget(); + const auto &content = _badgeContent.current(); + const auto &collectible = content.emojiStatusId.collectible; + const auto premium = (content.badge == BadgeType::Premium); + const auto id = (collectible && widget && premium) + ? collectible->id + : uint64(); + if (_badgeCollectibleId == id) { + return; + } + hideBadgeTooltip(); + if (!collectible) { + return; + } + const auto parent = _parentForTooltip + ? _parentForTooltip() + : _controller->window().widget()->bodyWidget(); + _badgeTooltip = std::make_unique( + parent, + collectible, + widget); + const auto raw = _badgeTooltip.get(); + raw->fade(true); + _badgeTooltipHide.callOnce(kGiftBadgeGlares * raw->glarePeriod() + - st::infoGiftTooltip.duration * 1.5); + }, lifetime()); + + if (const auto raw = _badgeTooltip.get()) { + raw->finishAnimating(); + } +} + } // namespace Info::Profile diff --git a/Telegram/SourceFiles/info/profile/info_profile_cover.h b/Telegram/SourceFiles/info/profile/info_profile_cover.h index 9b972fc36..5add87fc7 100644 --- a/Telegram/SourceFiles/info/profile/info_profile_cover.h +++ b/Telegram/SourceFiles/info/profile/info_profile_cover.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "info/profile/info_profile_badge.h" #include "ui/wrap/padding_wrap.h" #include "ui/abstract_button.h" #include "base/timer.h" @@ -103,7 +104,8 @@ public: Cover( QWidget *parent, not_null controller, - not_null peer); + not_null peer, + Fn()> parentForTooltip = nullptr); Cover( QWidget *parent, not_null controller, @@ -124,13 +126,16 @@ public: [[nodiscard]] std::optional updatedPersonalPhoto() const; private: + class BadgeTooltip; + Cover( QWidget *parent, not_null controller, not_null peer, Data::ForumTopic *topic, Role role, - rpl::producer title); + rpl::producer title, + Fn()> parentForTooltip); void setupShowLastSeen(); void setupChildGeometry(); @@ -139,7 +144,9 @@ private: void refreshNameGeometry(int newWidth); void refreshStatusGeometry(int newWidth); void refreshUploadPhotoOverlay(); + void setupUniqueBadgeTooltip(); void setupChangePersonal(); + void hideBadgeTooltip(); const style::InfoProfileCover &_st; @@ -148,10 +155,17 @@ private: const not_null _peer; const std::unique_ptr _emojiStatusPanel; const std::unique_ptr _botVerify; + rpl::variable _badgeContent; const std::unique_ptr _badge; const std::unique_ptr _verified; rpl::variable _onlineCount; + const Fn()> _parentForTooltip; + std::unique_ptr _badgeTooltip; + std::vector> _badgeOldTooltips; + base::Timer _badgeTooltipHide; + uint64 _badgeCollectibleId = 0; + const object_ptr _userpic; Ui::UserpicButton *_changePersonal = nullptr; std::optional _personalChosen;