From 561e3f4809c317963d7732bed8d0a8cfc53e0eb5 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 14 Nov 2022 11:24:31 +0400 Subject: [PATCH] Handle clicks on topic jump area. --- .../dialogs/dialogs_inner_widget.cpp | 99 +++++++-- .../dialogs/dialogs_inner_widget.h | 10 +- Telegram/SourceFiles/dialogs/dialogs_row.cpp | 68 +++++- Telegram/SourceFiles/dialogs/dialogs_row.h | 22 +- .../SourceFiles/dialogs/dialogs_widget.cpp | 15 +- .../SourceFiles/dialogs/ui/dialogs_layout.cpp | 14 +- .../SourceFiles/dialogs/ui/dialogs_layout.h | 2 + .../dialogs/ui/dialogs_message_view.cpp | 132 ++++-------- .../dialogs/ui/dialogs_message_view.h | 19 +- .../dialogs/ui/dialogs_topics_view.cpp | 195 ++++++++++++++++++ .../dialogs/ui/dialogs_topics_view.h | 51 +++++ 11 files changed, 486 insertions(+), 141 deletions(-) diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index 90bca873c..f9a5d15e5 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -156,6 +156,11 @@ InnerWidget::InnerWidget( _cancelSearchInChat->hide(); _cancelSearchFromUser->hide(); + style::PaletteChanged( + ) | rpl::start_with_next([=] { + _topicJumpCache = nullptr; + }, lifetime()); + session().downloaderTaskFinished( ) | rpl::start_with_next([=] { update(); @@ -336,7 +341,7 @@ void InnerWidget::refreshWithCollapsedRows(bool toTop) { _selected = nullptr; } if (_pressed && _pressed->folder() == archive) { - setPressed(nullptr); + clearPressed(); } _skipTopDialog = true; if (!inMainMenu && !_filterId) { @@ -538,6 +543,9 @@ void InnerWidget::paintEvent(QPaintEvent *e) { .selected = (_menuRow.key ? (row->key() == _menuRow.key) : selected), + .topicJumpSelected = (selected + && _selectedTopicJump + && (!_pressed || _pressedTopicJump)), .paused = videoPaused, .narrow = (fullWidth < st::columnMinimalWidthLeft), }); @@ -1142,7 +1150,7 @@ void InnerWidget::clearIrrelevantState() { _collapsedSelected = -1; setCollapsedPressed(-1); _selected = nullptr; - setPressed(nullptr); + clearPressed(); } } @@ -1168,9 +1176,16 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { : (mouseY >= offset) ? _shownList->rowAtY(mouseY - offset) : nullptr; - if (_selected != selected || _collapsedSelected != collapsedSelected) { + const auto selectedTopicJump = selected + && selected->lookupIsInTopicJump( + local.x(), + mouseY - offset - selected->top()); + if (_collapsedSelected != collapsedSelected + || _selected != selected + || _selectedTopicJump != selectedTopicJump) { updateSelectedRow(); _selected = selected; + _selectedTopicJump = selectedTopicJump; _collapsedSelected = collapsedSelected; updateSelectedRow(); setCursor((_selected || _collapsedSelected) @@ -1203,9 +1218,15 @@ void InnerWidget::selectByMouse(QPoint globalPosition) { if (filteredSelected < 0 || filteredSelected >= _filterResults.size()) { filteredSelected = -1; } - if (_filteredSelected != filteredSelected) { + const auto selectedTopicJump = (filteredSelected >= 0) + && _filterResults[filteredSelected].row->lookupIsInTopicJump( + local.x(), + mouseY - skip - _filterResults[filteredSelected].top); + if (_filteredSelected != filteredSelected + || _selectedTopicJump != selectedTopicJump) { updateSelectedRow(); _filteredSelected = filteredSelected; + _selectedTopicJump = selectedTopicJump; updateSelectedRow(); } } @@ -1243,7 +1264,7 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { selectByMouse(e->globalPos()); _pressButton = e->button(); - setPressed(_selected); + setPressed(_selected, _selectedTopicJump); setCollapsedPressed(_collapsedSelected); setHashtagPressed(_hashtagSelected); _hashtagDeletePressed = _hashtagDeleteSelected; @@ -1257,14 +1278,25 @@ void InnerWidget::mousePressEvent(QMouseEvent *e) { }); } else if (_pressed) { auto row = _pressed; - row->addRipple( - e->pos() - QPoint(0, dialogsOffset() + _pressed->top()), - QSize(width(), _pressed->height()), - [this, row] { - if (!_pinnedShiftAnimation.animating()) { - row->entry()->updateChatListEntry(); - } - }); + const auto updateCallback = [this, row] { + if (!_pinnedShiftAnimation.animating()) { + row->entry()->updateChatListEntry(); + } + }; + const auto origin = e->pos() + - QPoint(0, dialogsOffset() + _pressed->top()); + if (_pressedTopicJump) { + row->addTopicJumpRipple( + origin, + _topicJumpCache.get(), + updateCallback); + } else { + row->clearTopicJumpRipple(); + row->addRipple( + origin, + QSize(width(), _pressed->height()), + updateCallback); + } _dragStart = e->pos(); } else if (base::in_range(_hashtagPressed, 0, _hashtagResults.size()) && !_hashtagDeletePressed) { auto row = &_hashtagResults[_hashtagPressed]->row; @@ -1543,8 +1575,10 @@ void InnerWidget::mousePressReleased( auto collapsedPressed = _collapsedPressed; setCollapsedPressed(-1); + const auto pressedTopicRootId = _pressedTopicJumpRootId; + const auto pressedTopicJump = _pressedTopicJump; auto pressed = _pressed; - setPressed(nullptr); + clearPressed(); auto hashtagPressed = _hashtagPressed; setHashtagPressed(-1); auto hashtagDeletePressed = _hashtagDeletePressed; @@ -1561,7 +1595,9 @@ void InnerWidget::mousePressReleased( updateSelectedRow(); if (!wasDragging && button == Qt::LeftButton) { if ((collapsedPressed >= 0 && collapsedPressed == _collapsedSelected) - || (pressed && pressed == _selected) + || (pressed + && pressed == _selected + && pressedTopicJump == _selectedTopicJump) || (hashtagPressed >= 0 && hashtagPressed == _hashtagSelected && hashtagDeletePressed == _hashtagDeleteSelected) @@ -1570,7 +1606,7 @@ void InnerWidget::mousePressReleased( && peerSearchPressed == _peerSearchSelected) || (searchedPressed >= 0 && searchedPressed == _searchedSelected)) { - chooseRow(modifiers); + chooseRow(modifiers, pressedTopicRootId); } } } @@ -1584,15 +1620,23 @@ void InnerWidget::setCollapsedPressed(int pressed) { } } -void InnerWidget::setPressed(Row *pressed) { - if (_pressed != pressed) { +void InnerWidget::setPressed(Row *pressed, bool pressedTopicJump) { + if (_pressed != pressed || _pressedTopicJump != pressedTopicJump) { if (_pressed) { _pressed->stopLastRipple(); } _pressed = pressed; + _pressedTopicJump = pressedTopicJump; + const auto history = pressedTopicJump ? pressed->history() : nullptr; + const auto item = history ? history->chatListMessage() : nullptr; + _pressedTopicJumpRootId = item ? item->topicRootId() : MsgId(); } } +void InnerWidget::clearPressed() { + setPressed(nullptr, false); +} + void InnerWidget::setHashtagPressed(int pressed) { if (base::in_range(_hashtagPressed, 0, _hashtagResults.size())) { _hashtagResults[_hashtagPressed]->row.stopLastRipple(); @@ -1656,7 +1700,7 @@ void InnerWidget::dialogRowReplaced( _selected = newRow; } if (_pressed == oldRow) { - setPressed(newRow); + setPressed(newRow, _pressedTopicJump); } if (_dragging == oldRow) { if (newRow) { @@ -1704,7 +1748,7 @@ void InnerWidget::handleChatListEntryRefreshes() { _selected = nullptr; } if (_pressed && _pressed->key() == key) { - setPressed(nullptr); + clearPressed(); } const auto i = ranges::find( _filterResults, @@ -3147,7 +3191,9 @@ ChosenRow InnerWidget::computeChosenRow() const { return ChosenRow(); } -bool InnerWidget::chooseRow(Qt::KeyboardModifiers modifiers) { +bool InnerWidget::chooseRow( + Qt::KeyboardModifiers modifiers, + MsgId pressedTopicRootId) { if (chooseCollapsedRow()) { return true; } else if (chooseHashtag()) { @@ -3161,11 +3207,20 @@ bool InnerWidget::chooseRow(Qt::KeyboardModifiers modifiers) { } return row; }; - const auto chosen = modifyChosenRow(computeChosenRow(), modifiers); + auto chosen = modifyChosenRow(computeChosenRow(), modifiers); if (chosen.key) { if (IsServerMsgId(chosen.message.fullId.msg)) { session().local().saveRecentSearchHashtags(_filter); } + if (pressedTopicRootId && !chosen.message.fullId) { + const auto history = chosen.key.history(); + if (history->peer->isForum()) { + chosen.message.fullId = { + history->peer->id, + pressedTopicRootId, + }; + } + } _chosenRow.fire_copy(chosen); return true; } diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index 095d97b71..7ed2917a9 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -113,7 +113,9 @@ public: void refreshEmptyLabel(); void resizeEmptyLabel(); - bool chooseRow(Qt::KeyboardModifiers modifiers = {}); + bool chooseRow( + Qt::KeyboardModifiers modifiers = {}, + MsgId pressedTopicRootId = {}); void scrollToEntry(const RowDescriptor &entry); @@ -240,7 +242,8 @@ private: void scrollToItem(int top, int height); void scrollToDefaultSelected(); void setCollapsedPressed(int pressed); - void setPressed(Row *pressed); + void setPressed(Row *pressed, bool pressedTopicJump); + void clearPressed(); void setHashtagPressed(int pressed); void setFilteredPressed(int pressed); void setPeerSearchPressed(int pressed); @@ -404,6 +407,9 @@ private: bool _skipTopDialog = false; Row *_selected = nullptr; Row *_pressed = nullptr; + MsgId _pressedTopicJumpRootId; + bool _selectedTopicJump = false; + bool _pressedTopicJump = false; Row *_dragging = nullptr; int _draggingIndex = -1; diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index aaa6ce765..4e38845e0 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -35,15 +35,30 @@ void BasicRow::addRipple( QSize size, Fn updateCallback) { if (!_ripple) { - auto mask = Ui::RippleAnimation::RectMask(size); - _ripple = std::make_unique( - st::dialogsRipple, - std::move(mask), + addRippleWithMask( + origin, + Ui::RippleAnimation::RectMask(size), std::move(updateCallback)); + } else { + _ripple->add(origin); } +} + +void BasicRow::addRippleWithMask( + QPoint origin, + QImage mask, + Fn updateCallback) { + _ripple = std::make_unique( + st::dialogsRipple, + std::move(mask), + std::move(updateCallback)); _ripple->add(origin); } +void BasicRow::clearRipple() { + _ripple = nullptr; +} + void BasicRow::stopLastRipple() { if (_ripple) { _ripple->lastStop(); @@ -102,10 +117,11 @@ uint64 Row::sortKey(FilterId filterId) const { void Row::setCornerBadgeShown( bool shown, Fn updateCallback) const { - if (_cornerBadgeShown == shown) { + const auto value = (shown ? 1 : 0); + if (_cornerBadgeShown == value) { return; } - const_cast(this)->_cornerBadgeShown = shown; + const_cast(this)->_cornerBadgeShown = value; if (_cornerBadgeUserpic && _cornerBadgeUserpic->animation.animating()) { _cornerBadgeUserpic->animation.change( _cornerBadgeShown ? 1. : 0., @@ -279,6 +295,46 @@ void Row::paintUserpic( p.setOpacity(1.); } +bool Row::lookupIsInTopicJump(int x, int y) const { + const auto history = this->history(); + return history && history->lastItemDialogsView().isInTopicJump(x, y); +} + +void Row::stopLastRipple() { + BasicRow::stopLastRipple(); + const auto history = this->history(); + const auto view = history ? &history->lastItemDialogsView() : nullptr; + if (view) { + view->stopLastRipple(); + } +} + +void Row::addTopicJumpRipple( + QPoint origin, + not_null topicJumpCache, + Fn updateCallback) { + const auto history = this->history(); + const auto view = history ? &history->lastItemDialogsView() : nullptr; + if (view) { + view->addTopicJumpRipple( + origin, + topicJumpCache, + std::move(updateCallback)); + _topicJumpRipple = 1; + } +} + +void Row::clearTopicJumpRipple() { + if (_topicJumpRipple) { + clearRipple(); + _topicJumpRipple = 0; + } +} + +bool Row::topicJumpRipple() const { + return _topicJumpRipple != 0; +} + FakeRow::FakeRow( Key searchInChat, not_null item, diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h index 74f47e9cb..4df2fd36f 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.h +++ b/Telegram/SourceFiles/dialogs/dialogs_row.h @@ -33,6 +33,7 @@ using namespace ::Ui; class RowPainter; class VideoUserpic; struct PaintContext; +struct TopicJumpCache; } // namespace Dialogs::Ui namespace Dialogs { @@ -52,7 +53,12 @@ public: const Ui::PaintContext &context) const; void addRipple(QPoint origin, QSize size, Fn updateCallback); - void stopLastRipple(); + virtual void stopLastRipple(); + void addRippleWithMask( + QPoint origin, + QImage mask, + Fn updateCallback); + void clearRipple(); void paintRipple( QPainter &p, @@ -96,6 +102,15 @@ public: History *historyForCornerBadge, const Ui::PaintContext &context) const final override; + [[nodiscard]] bool lookupIsInTopicJump(int x, int y) const; + void stopLastRipple() override; + void addTopicJumpRipple( + QPoint origin, + not_null topicJumpCache, + Fn updateCallback); + void clearTopicJumpRipple(); + [[nodiscard]] bool topicJumpRipple() const; + [[nodiscard]] Key key() const { return _id; } @@ -149,8 +164,9 @@ private: mutable std::unique_ptr _cornerBadgeUserpic; int _top = 0; int _height = 0; - int _index = 0; - bool _cornerBadgeShown = false; + int _index : 30 = 0; + int _cornerBadgeShown : 1 = 0; + int _topicJumpRipple : 1 = 0; }; diff --git a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp index 6bf5291d2..0ae32551c 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_widget.cpp @@ -415,8 +415,19 @@ void Widget::chosenRow(const ChosenRow &row) { const auto openSearchResult = !controller()->selectingPeer() && row.filteredRow; const auto history = row.key.history(); - if (const auto topic = row.key.topic()) { - controller()->content()->chooseThread(topic, row.message.fullId.msg); + const auto topicJump = history + ? history->peer->forumTopicFor(row.message.fullId.msg) + : nullptr; + if (topicJump) { + controller()->openForum(history->peer->asChannel()); + controller()->content()->chooseThread( + topicJump, + ShowAtUnreadMsgId); + return; + } else if (const auto topic = row.key.topic()) { + controller()->content()->chooseThread( + topic, + row.message.fullId.msg); } else if (history && history->peer->isForum() && !row.message.fullId) { controller()->openForum(history->peer->asChannel()); return; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 755179b43..8af53aab0 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -232,6 +232,7 @@ enum class Flag { SavedMessages = 0x08, RepliesMessages = 0x10, AllowUserOnline = 0x20, + TopicJumpRipple = 0x40, }; inline constexpr bool is_flag_type(Flag) { return true; } @@ -264,11 +265,13 @@ void PaintRow( : context.selected ? st::dialogsBgOver : st::dialogsBg; - auto ripple = context.active - ? st::dialogsRippleBgActive - : st::dialogsRippleBg; p.fillRect(geometry, bg); - row->paintRipple(p, 0, 0, context.width, &ripple->c); + if (!(flags & Flag::TopicJumpRipple)) { + auto ripple = context.active + ? st::dialogsRippleBgActive + : st::dialogsRippleBg; + row->paintRipple(p, 0, 0, context.width, &ripple->c); + } const auto history = entry->asHistory(); const auto thread = entry->asThread(); @@ -883,7 +886,8 @@ void RowPainter::Paint( const auto allowUserOnline = !context.narrow || badgesState.empty(); const auto flags = (allowUserOnline ? Flag::AllowUserOnline : Flag(0)) | (peer && peer->isSelf() ? Flag::SavedMessages : Flag(0)) - | (peer && peer->isRepliesChat() ? Flag::RepliesMessages : Flag(0)); + | (peer && peer->isRepliesChat() ? Flag::RepliesMessages : Flag(0)) + | (row->topicJumpRipple() ? Flag::TopicJumpRipple : Flag(0)); const auto paintItemCallback = [&](int nameleft, int namewidth) { const auto texttop = context.st->textTop; const auto availableWidth = PaintWideCounter( diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h index c626161ac..0fd29dd19 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h @@ -48,6 +48,7 @@ struct TopicJumpCorners { struct TopicJumpCache { TopicJumpCorners corners; TopicJumpCorners over; + TopicJumpCorners selected; TopicJumpCorners rippleMask; }; @@ -61,6 +62,7 @@ struct PaintContext { int width = 0; bool active = false; bool selected = false; + bool topicJumpSelected = false; bool paused = false; bool search = false; bool narrow = false; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp index ff6bed930..1e05f9e57 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp @@ -200,6 +200,28 @@ void MessageView::prepare( } } +bool MessageView::isInTopicJump(int x, int y) const { + return _topics && _topics->isInTopicJumpArea(x, y); +} + +void MessageView::addTopicJumpRipple( + QPoint origin, + not_null topicJumpCache, + Fn updateCallback) { + if (_topics) { + _topics->addTopicJumpRipple( + origin, + topicJumpCache, + std::move(updateCallback)); + } +} + +void MessageView::stopLastRipple() { + if (_topics) { + _topics->stopLastRipple(); + } +} + int MessageView::countWidth() const { auto result = 0; if (!_senderCache.isEmpty()) { @@ -248,6 +270,8 @@ void MessageView::paint( const auto jump1 = checkJump ? _topics->jumpToTopicWidth() : 0; if (jump1) { paintJumpToLast(p, rect, context, jump1); + } else if (_topics) { + _topics->clearTopicJumpGeometry(); } if (withTopic) { @@ -320,9 +344,11 @@ void MessageView::paintJumpToLast( const PaintContext &context, int width1) const { if (!context.topicJumpCache) { + _topics->clearTopicJumpGeometry(); return; } - FillJumpToLastBg(p, { + const auto width2 = countWidth() + st::forumDialogJumpArrowSkip; + const auto geometry = FillJumpToLastBg(p, { .st = context.st, .corners = (context.selected ? &context.topicJumpCache->over @@ -332,97 +358,23 @@ void MessageView::paintJumpToLast( ? st::dialogsRippleBg : st::dialogsBgOver), .width1 = width1, - .width2 = countWidth() + st::forumDialogJumpArrowSkip, + .width2 = width2, }); -} - -void FillJumpToLastBg(QPainter &p, JumpToLastBg context) { - const auto availableWidth = context.geometry.width(); - const auto use1 = std::min(context.width1, availableWidth); - const auto use2 = std::min(context.width2, availableWidth); - const auto padding = st::forumDialogJumpPadding; - const auto radius = st::forumDialogJumpRadius; - const auto &bg = context.bg; - auto &normal = context.corners->normal; - auto &inverted = context.corners->inverted; - auto &small = context.corners->small; - auto hq = PainterHighQualityEnabler(p); - p.setPen(Qt::NoPen); - p.setBrush(bg); - const auto origin = context.geometry.topLeft(); - const auto delta = std::abs(use1 - use2); - if (delta <= context.st->topicsSkip / 2) { - if (normal.p[0].isNull()) { - normal = Ui::PrepareCornerPixmaps(radius, bg); - } - const auto w = std::max(use1, use2); - const auto h = context.st->topicsHeight + st::normalFont->height; - const auto fill = QRect(origin, QSize(w, h)); - Ui::FillRoundRect(p, fill.marginsAdded(padding), bg, normal); - } else { - const auto h1 = context.st->topicsHeight; - const auto h2 = st::normalFont->height; - const auto hmin = std::min(h1, h2); - const auto wantedInvertedRadius = hmin - radius; - const auto invertedr = std::min(wantedInvertedRadius, delta / 2); - const auto smallr = std::min(radius, delta - invertedr); - const auto smallkey = (use1 < use2) ? smallr : (-smallr); - if (normal.p[0].isNull()) { - normal = Ui::PrepareCornerPixmaps(radius, bg); - } - if (inverted.p[0].isNull() - || context.corners->invertedRadius != invertedr) { - context.corners->invertedRadius = invertedr; - inverted = Ui::PrepareInvertedCornerPixmaps(invertedr, bg); - } - if (smallr != radius - && (small.isNull() || context.corners->smallKey != smallkey)) { - context.corners->smallKey = smallr; - auto pixmaps = Ui::PrepareCornerPixmaps(smallr, bg); - small = pixmaps.p[(use1 < use2) ? 1 : 3]; - } - const auto rect1 = QRect(origin, QSize(use1, h1)); - auto no1 = normal; - no1.p[2] = QPixmap(); - if (use1 < use2) { - no1.p[3] = QPixmap(); - } else if (smallr != radius) { - no1.p[3] = small; - } - auto fill1 = rect1.marginsAdded({ - padding.left(), - padding.top(), - padding.right(), - (use1 < use2 ? -padding.top() : padding.bottom()), + if (context.topicJumpSelected) { + p.setOpacity(0.1); + FillJumpToLastPrepared(p, { + .st = context.st, + .corners = &context.topicJumpCache->selected, + .bg = st::dialogsTextFg, + .prepared = geometry, }); - Ui::FillRoundRect(p, fill1, bg, no1); - if (use1 < use2) { - p.drawPixmap( - fill1.x() + fill1.width(), - fill1.y() + fill1.height() - invertedr, - inverted.p[3]); - } - const auto add = QPoint(0, h1); - const auto rect2 = QRect(origin + add, QSize(use2, h2)); - const auto fill2 = rect2.marginsAdded({ - padding.left(), - (use2 < use1 ? -padding.bottom() : padding.top()), - padding.right(), - padding.bottom(), - }); - auto no2 = normal; - no2.p[0] = QPixmap(); - if (use2 < use1) { - no2.p[1] = QPixmap(); - } else if (smallr != radius) { - no2.p[1] = small; - } - Ui::FillRoundRect(p, fill2, bg, no2); - if (use2 < use1) { - p.drawPixmap( - fill2.x() + fill2.width(), - fill2.y(), - inverted.p[0]); + p.setOpacity(1.); + } + if (!_topics->changeTopicJumpGeometry(geometry)) { + auto color = st::dialogsTextFg->c; + color.setAlpha(color.alpha() / 10); + if (color.alpha() > 0) { + _topics->paintRipple(p, 0, 0, context.width, &color); } } } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h index 921d6cb3d..7718f3c64 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h @@ -35,8 +35,8 @@ namespace Dialogs::Ui { using namespace ::Ui; struct PaintContext; +struct TopicJumpCache; class TopicsView; -struct TopicJumpCorners; [[nodiscard]] TextWithEntities DialogsPreviewText(TextWithEntities text); @@ -66,6 +66,13 @@ public: const QRect &geometry, const PaintContext &context) const; + [[nodiscard]] bool isInTopicJump(int x, int y) const; + void addTopicJumpRipple( + QPoint origin, + not_null topicJumpCache, + Fn updateCallback); + void stopLastRipple(); + private: struct LoadingContext; @@ -90,14 +97,4 @@ private: const QString &sender, TextWithEntities topic); -struct JumpToLastBg { - not_null st; - not_null corners; - QRect geometry; - const style::color &bg; - int width1 = 0; - int width2 = 0; -}; -void FillJumpToLastBg(QPainter &p, JumpToLastBg context); - } // namespace Dialogs::Ui diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp index f06ba3777..8c899238c 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/painter.h" #include "ui/text/text_options.h" #include "ui/text/text_utilities.h" +#include "ui/effects/ripple_animation.h" #include "styles/style_dialogs.h" namespace Dialogs::Ui { @@ -142,4 +143,198 @@ void TopicsView::paint( } } +bool TopicsView::changeTopicJumpGeometry(JumpToLastGeometry geometry) { + if (_lastTopicJumpGeometry != geometry) { + _lastTopicJumpGeometry = geometry; + return true; + } + return false; +} + +void TopicsView::clearTopicJumpGeometry() { + changeTopicJumpGeometry({}); +} + +bool TopicsView::isInTopicJumpArea(int x, int y) const { + return _lastTopicJumpGeometry.area1.contains(x, y) + || _lastTopicJumpGeometry.area2.contains(x, y); +} + +void TopicsView::addTopicJumpRipple( + QPoint origin, + not_null topicJumpCache, + Fn updateCallback) { + auto mask = topicJumpRippleMask(topicJumpCache); + if (mask.isNull()) { + return; + } + _ripple = std::make_unique( + st::dialogsRipple, + std::move(mask), + std::move(updateCallback)); + _ripple->add(origin); +} + +void TopicsView::stopLastRipple() { + if (_ripple) { + _ripple->lastStop(); + } +} + +void TopicsView::paintRipple( + QPainter &p, + int x, + int y, + int outerWidth, + const QColor *colorOverride) const { + if (_ripple) { + _ripple->paint(p, x, y, outerWidth, colorOverride); + if (_ripple->empty()) { + _ripple.reset(); + } + } +} + +QImage TopicsView::topicJumpRippleMask( + not_null topicJumpCache) const { + const auto &st = st::forumDialogRow; + const auto area1 = _lastTopicJumpGeometry.area1; + if (area1.isEmpty()) { + return QImage(); + } + const auto area2 = _lastTopicJumpGeometry.area2; + const auto drawer = [&](QPainter &p) { + const auto white = style::complex_color([] { return Qt::white; }); + // p.setOpacity(.1); + FillJumpToLastPrepared(p, { + .st = &st, + .corners = &topicJumpCache->rippleMask, + .bg = white.color(), + .prepared = _lastTopicJumpGeometry, + }); + }; + return Ui::RippleAnimation::MaskByDrawer( + QRect(0, 0, 1, 1).united(area1).united(area2).size(), + false, + drawer); +} + +JumpToLastGeometry FillJumpToLastBg(QPainter &p, JumpToLastBg context) { + const auto availableWidth = context.geometry.width(); + const auto use1 = std::min(context.width1, availableWidth); + const auto use2 = std::min(context.width2, availableWidth); + const auto padding = st::forumDialogJumpPadding; + const auto radius = st::forumDialogJumpRadius; + const auto origin = context.geometry.topLeft(); + const auto delta = std::abs(use1 - use2); + if (delta <= context.st->topicsSkip / 2) { + const auto w = std::max(use1, use2); + const auto h = context.st->topicsHeight + st::normalFont->height; + const auto fill = QRect(origin, QSize(w, h)); + const auto full = fill.marginsAdded(padding); + auto result = JumpToLastGeometry{ full }; + FillJumpToLastPrepared(p, { + .st = context.st, + .corners = context.corners, + .bg = context.bg, + .prepared = result, + }); + return result; + } + const auto h1 = context.st->topicsHeight; + const auto h2 = st::normalFont->height; + const auto rect1 = QRect(origin, QSize(use1, h1)); + const auto fill1 = rect1.marginsAdded({ + padding.left(), + padding.top(), + padding.right(), + (use1 < use2 ? -padding.top() : padding.bottom()), + }); + const auto add = QPoint(0, h1); + const auto rect2 = QRect(origin + add, QSize(use2, h2)); + const auto fill2 = rect2.marginsAdded({ + padding.left(), + (use2 < use1 ? -padding.bottom() : padding.top()), + padding.right(), + padding.bottom(), + }); + auto result = JumpToLastGeometry{ fill1, fill2 }; + FillJumpToLastPrepared(p, { + .st = context.st, + .corners = context.corners, + .bg = context.bg, + .prepared = result, + }); + return result; +} + +void FillJumpToLastPrepared(QPainter &p, JumpToLastPrepared context) { + auto &normal = context.corners->normal; + auto &inverted = context.corners->inverted; + auto &small = context.corners->small; + const auto radius = st::forumDialogJumpRadius; + const auto &bg = context.bg; + const auto area1 = context.prepared.area1; + const auto area2 = context.prepared.area2; + if (area2.isNull()) { + if (normal.p[0].isNull()) { + normal = Ui::PrepareCornerPixmaps(radius, bg); + } + Ui::FillRoundRect(p, area1, bg, normal); + return; + } + const auto width1 = area1.width(); + const auto width2 = area2.width(); + const auto delta = std::abs(width1 - width2); + const auto h1 = context.st->topicsHeight; + const auto h2 = st::normalFont->height; + const auto hmin = std::min(h1, h2); + const auto wantedInvertedRadius = hmin - radius; + const auto invertedr = std::min(wantedInvertedRadius, delta / 2); + const auto smallr = std::min(radius, delta - invertedr); + const auto smallkey = (width1 < width2) ? smallr : (-smallr); + if (normal.p[0].isNull()) { + normal = Ui::PrepareCornerPixmaps(radius, bg); + } + if (inverted.p[0].isNull() + || context.corners->invertedRadius != invertedr) { + context.corners->invertedRadius = invertedr; + inverted = Ui::PrepareInvertedCornerPixmaps(invertedr, bg); + } + if (smallr != radius + && (small.isNull() || context.corners->smallKey != smallkey)) { + context.corners->smallKey = smallr; + auto pixmaps = Ui::PrepareCornerPixmaps(smallr, bg); + small = pixmaps.p[(width1 < width2) ? 1 : 3]; + } + auto no1 = normal; + no1.p[2] = QPixmap(); + if (width1 < width2) { + no1.p[3] = QPixmap(); + } else if (smallr != radius) { + no1.p[3] = small; + } + Ui::FillRoundRect(p, area1, bg, no1); + if (width1 < width2) { + p.drawPixmap( + area1.x() + width1, + area1.y() + area1.height() - invertedr, + inverted.p[3]); + } + auto no2 = normal; + no2.p[0] = QPixmap(); + if (width2 < width1) { + no2.p[1] = QPixmap(); + } else if (smallr != radius) { + no2.p[1] = small; + } + Ui::FillRoundRect(p, area2, bg, no2); + if (width2 < width1) { + p.drawPixmap( + area2.x() + width2, + area2.y(), + inverted.p[0]); + } +} + } // namespace Dialogs::Ui diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h index 5a05718fc..f0978a3d6 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h @@ -19,6 +19,7 @@ class ForumTopic; } // namespace Data namespace Ui { +class RippleAnimation; } // namespace Ui namespace Dialogs::Ui { @@ -26,6 +27,34 @@ namespace Dialogs::Ui { using namespace ::Ui; struct PaintContext; +struct TopicJumpCache; +struct TopicJumpCorners; + +struct JumpToLastBg { + not_null st; + not_null corners; + QRect geometry; + const style::color &bg; + int width1 = 0; + int width2 = 0; +}; +struct JumpToLastGeometry { + QRect area1; + QRect area2; + + friend inline auto operator<=>( + const JumpToLastGeometry&, + const JumpToLastGeometry&) = default; +}; +JumpToLastGeometry FillJumpToLastBg(QPainter &p, JumpToLastBg context); + +struct JumpToLastPrepared { + not_null st; + not_null corners; + const style::color &bg; + const JumpToLastGeometry &prepared; +}; +void FillJumpToLastPrepared(QPainter &p, JumpToLastPrepared context); class TopicsView final { public: @@ -46,6 +75,22 @@ public: const QRect &geometry, const PaintContext &context) const; + bool changeTopicJumpGeometry(JumpToLastGeometry geometry); + void clearTopicJumpGeometry(); + [[nodiscard]] bool isInTopicJumpArea(int x, int y) const; + void addTopicJumpRipple( + QPoint origin, + not_null topicJumpCache, + Fn updateCallback); + void paintRipple( + QPainter &p, + int x, + int y, + int outerWidth, + const QColor *colorOverride) const; + void clearRipple(); + void stopLastRipple(); + [[nodiscard]] rpl::lifetime &lifetime() { return _lifetime; } @@ -57,9 +102,15 @@ private: int version = -1; bool unread = false; }; + + [[nodiscard]] QImage topicJumpRippleMask( + not_null topicJumpCache) const; + const not_null _forum; mutable std::vector _titles; + mutable std::unique_ptr<RippleAnimation> _ripple; + JumpToLastGeometry _lastTopicJumpGeometry; int _version = -1; bool _jumpToTopic = false;