diff --git a/Telegram/SourceFiles/data/data_folder.cpp b/Telegram/SourceFiles/data/data_folder.cpp index 8c3b719092..b8ed32698a 100644 --- a/Telegram/SourceFiles/data/data_folder.cpp +++ b/Telegram/SourceFiles/data/data_folder.cpp @@ -16,6 +16,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history.h" #include "history/history_item.h" #include "ui/painter.h" +#include "ui/text/text_options.h" +#include "ui/text/text_utilities.h" #include "lang/lang_keys.h" #include "storage/storage_facade.h" #include "core/application.h" @@ -33,9 +35,67 @@ namespace { constexpr auto kLoadedChatsMinCount = 20; constexpr auto kShowChatNamesCount = 8; +[[nodiscard]] TextWithEntities ComposeFolderListEntryText( + not_null folder) { + const auto &list = folder->lastHistories(); + if (list.empty()) { + return {}; + } + + const auto count = std::max( + int(list.size()), + folder->chatsList()->fullSize().current()); + + const auto throwAwayLastName = (list.size() > 1) + && (count == list.size() + 1); + auto &&peers = ranges::views::all( + list + ) | ranges::views::take( + list.size() - (throwAwayLastName ? 1 : 0) + ); + const auto wrapName = [](not_null history) { + const auto name = history->peer->name(); + return TextWithEntities{ + .text = name, + .entities = (history->chatListBadgesState().unread + ? EntitiesInText{ + { EntityType::Semibold, 0, int(name.size()), QString() }, + { EntityType::PlainLink, 0, int(name.size()), QString() }, + } + : EntitiesInText{}), + }; + }; + const auto shown = int(peers.size()); + const auto accumulated = [&] { + Expects(shown > 0); + + auto i = peers.begin(); + auto result = wrapName(*i); + for (++i; i != peers.end(); ++i) { + result = tr::lng_archived_last_list( + tr::now, + lt_accumulated, + result, + lt_chat, + wrapName(*i), + Ui::Text::WithEntities); + } + return result; + }(); + return (shown < count) + ? tr::lng_archived_last( + tr::now, + lt_count, + (count - shown), + lt_chats, + accumulated, + Ui::Text::WithEntities) + : accumulated; +} + } // namespace -Folder::Folder(not_null owner, FolderId id) +Folder::Folder(not_null owner, FolderId id) : Entry(owner, Type::Folder) , _id(id) , _chatsList( @@ -76,29 +136,6 @@ FolderId Folder::id() const { void Folder::indexNameParts() { // We don't want archive to be filtered in the chats list. - //_nameWords.clear(); - //_nameFirstLetters.clear(); - //auto toIndexList = QStringList(); - //auto appendToIndex = [&](const QString &value) { - // if (!value.isEmpty()) { - // toIndexList.push_back(TextUtilities::RemoveAccents(value)); - // } - //}; - - //appendToIndex(_name); - //const auto appendTranslit = !toIndexList.isEmpty() - // && cRussianLetters().match(toIndexList.front()).hasMatch(); - //if (appendTranslit) { - // appendToIndex(translitRusEng(toIndexList.front())); - //} - //auto toIndex = toIndexList.join(' '); - //toIndex += ' ' + rusKeyboardLayoutSwitch(toIndex); - - //const auto namesList = TextUtilities::PrepareSearchWords(toIndex); - //for (const auto &name : namesList) { - // _nameWords.insert(name); - // _nameFirstLetters.insert(name[0]); - //} } void Folder::registerOne(not_null history) { @@ -110,7 +147,6 @@ void Folder::registerOne(not_null history) { } else { updateChatListEntry(); } - applyChatListMessage(history->chatListMessage()); reorderLastHistories(); } @@ -118,9 +154,6 @@ void Folder::unregisterOne(not_null history) { if (_chatsList.empty()) { updateChatListExistence(); } - if (_chatListMessage && _chatListMessage->history() == history) { - computeChatListMessage(); - } reorderLastHistories(); } @@ -129,47 +162,11 @@ int Folder::chatListNameVersion() const { } void Folder::oneListMessageChanged(HistoryItem *from, HistoryItem *to) { - if (!applyChatListMessage(to) && _chatListMessage == from) { - computeChatListMessage(); - } if (from || to) { reorderLastHistories(); } } -bool Folder::applyChatListMessage(HistoryItem *item) { - if (!item) { - return false; - } else if (_chatListMessage - && _chatListMessage->date() >= item->date()) { - return false; - } - _chatListMessage = item; - updateChatListEntry(); - return true; -} - -void Folder::computeChatListMessage() { - auto &&items = ranges::views::all( - *_chatsList.indexed() - ) | ranges::views::filter([](not_null row) { - return row->entry()->chatListMessage() != nullptr; - }); - const auto chatListDate = [](not_null row) { - return row->entry()->chatListMessage()->date(); - }; - const auto top = ranges::max_element( - items, - ranges::less(), - chatListDate); - if (top == items.end()) { - _chatListMessage = nullptr; - } else { - _chatListMessage = (*top)->entry()->chatListMessage(); - } - updateChatListEntry(); -} - void Folder::reorderLastHistories() { // We want first kShowChatNamesCount histories, by last message date. const auto pred = [](not_null a, not_null b) { @@ -219,7 +216,7 @@ void Folder::loadUserpic() { void Folder::paintUserpic( Painter &p, - std::shared_ptr &view, + std::shared_ptr &view, const Dialogs::Ui::PaintContext &context) const { paintUserpic( p, @@ -288,8 +285,16 @@ const std::vector> &Folder::lastHistories() const { return _lastHistories; } -uint32 Folder::chatListViewVersion() const { - return _chatListViewVersion; +void Folder::validateListEntryCache() { + if (_listEntryCacheVersion == _chatListViewVersion) { + return; + } + _listEntryCacheVersion = _chatListViewVersion; + _listEntryCache.setMarkedText( + st::dialogsTextStyle, + ComposeFolderListEntryText(this), + // Use rich options as long as the entry text does not have user text. + Ui::ItemTextDefaultOptions()); } void Folder::requestChatListMessage() { @@ -299,7 +304,7 @@ void Folder::requestChatListMessage() { } TimeId Folder::adjustedChatListTimeId() const { - return _chatListMessage ? _chatListMessage->date() : chatListTimeId(); + return chatListTimeId(); } void Folder::applyDialog(const MTPDdialogFolder &data) { @@ -350,7 +355,7 @@ Dialogs::BadgesState Folder::chatListBadgesState() const { } HistoryItem *Folder::chatListMessage() const { - return _chatListMessage; + return nullptr; } bool Folder::chatListMessageKnown() const { diff --git a/Telegram/SourceFiles/data/data_folder.h b/Telegram/SourceFiles/data/data_folder.h index 4389851227..77805ef253 100644 --- a/Telegram/SourceFiles/data/data_folder.h +++ b/Telegram/SourceFiles/data/data_folder.h @@ -69,12 +69,13 @@ public: const style::color &overrideFg) const; const std::vector> &lastHistories() const; - uint32 chatListViewVersion() const; + void validateListEntryCache(); + [[nodiscard]] const Ui::Text::String &listEntryCache() const { + return _listEntryCache; + } private: void indexNameParts(); - bool applyChatListMessage(HistoryItem *item); - void computeChatListMessage(); int chatListNameVersion() const override; @@ -96,8 +97,10 @@ private: base::flat_set _nameFirstLetters; std::vector> _lastHistories; - HistoryItem *_chatListMessage = nullptr; - uint32 _chatListViewVersion = 0; + + Ui::Text::String _listEntryCache; + int _listEntryCacheVersion = 0; + int _chatListViewVersion = 0; //rpl::variable _unreadPosition; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/data/data_forum_topic.cpp b/Telegram/SourceFiles/data/data_forum_topic.cpp index f1ae83e4f9..bd6e537762 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.cpp +++ b/Telegram/SourceFiles/data/data_forum_topic.cpp @@ -625,6 +625,10 @@ TextWithEntities ForumTopic::titleWithIcon() const { return ForumTopicIconWithTitle(_iconId, _title); } +int ForumTopic::titleVersion() const { + return _titleVersion; +} + void ForumTopic::applyTitle(const QString &title) { if (_title == title) { return; @@ -647,6 +651,7 @@ void ForumTopic::applyIconId(DocumentId iconId) { return; } _iconId = iconId; + ++_titleVersion; _icon = iconId ? std::make_unique( owner().customEmojiManager().create( diff --git a/Telegram/SourceFiles/data/data_forum_topic.h b/Telegram/SourceFiles/data/data_forum_topic.h index 2bb335c63d..9ec4a971a6 100644 --- a/Telegram/SourceFiles/data/data_forum_topic.h +++ b/Telegram/SourceFiles/data/data_forum_topic.h @@ -114,6 +114,7 @@ public: [[nodiscard]] QString title() const; [[nodiscard]] TextWithEntities titleWithIcon() const; + [[nodiscard]] int titleVersion() const; void applyTitle(const QString &title); [[nodiscard]] DocumentId iconId() const; void applyIconId(DocumentId iconId); diff --git a/Telegram/SourceFiles/dialogs/dialogs.style b/Telegram/SourceFiles/dialogs/dialogs.style index 9994f6188a..510962dc6d 100644 --- a/Telegram/SourceFiles/dialogs/dialogs.style +++ b/Telegram/SourceFiles/dialogs/dialogs.style @@ -18,6 +18,7 @@ DialogRow { textLeft: pixels; textTop: pixels; topicsSkip: pixels; + topicsSkipBig: pixels; topicsHeight: pixels; } @@ -77,9 +78,18 @@ forumDialogRow: DialogRow(defaultDialogRow) { height: 80px; textTop: 32px; topicsSkip: 8px; - topicsHeight: 20px; + topicsSkipBig: 14px; + topicsHeight: 21px; } +forumDialogJumpArrow: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFg }}; +forumDialogJumpArrowOver: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFgOver }}; +forumDialogJumpArrowSkip: 8px; +forumDialogJumpArrowLeft: 3px; +forumDialogJumpArrowTop: 3px; +forumDialogJumpPadding: margins(8px, 3px, 8px, 3px); +forumDialogJumpRadius: 11px; + dialogsOnlineBadgeStroke: 2px; dialogsOnlineBadgeSize: 10px; dialogsOnlineBadgeSkip: point(0px, 2px); @@ -483,7 +493,3 @@ chooseTopicListItem: PeerListItem(defaultPeerListItem) { chooseTopicList: PeerList(defaultPeerList) { item: chooseTopicListItem; } - -dialogsTopicArrow: icon{{ "dialogs/dialogs_topic_arrow", dialogsTextFgService }}; -dialogsTopicArrowSkip: 13px; -dialogsTopicArrowTop: 4px; diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp index fbf03a4df8..90bca873c4 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.cpp @@ -514,22 +514,43 @@ void InnerWidget::paintEvent(QPaintEvent *e) { Window::GifPauseReason::Any); auto fullWidth = width(); auto dialogsClip = r; - auto ms = crl::now(); + const auto ms = crl::now(); + const auto paintRow = [&]( + not_null row, + bool selected, + bool mayBeActive) { + const auto key = row->key(); + const auto active = mayBeActive && (activeEntry.key == key); + const auto forum = key.history() + && key.history()->peer->isForum(); + if (forum && !_topicJumpCache) { + _topicJumpCache = std::make_unique(); + } + Ui::RowPainter::Paint(p, row, validateVideoUserpic(row), { + .st = (forum ? &st::forumDialogRow : _st.get()), + .topicJumpCache = _topicJumpCache.get(), + .folder = _openedFolder, + .forum = _openedForum, + .filter = _filterId, + .now = ms, + .width = fullWidth, + .active = active, + .selected = (_menuRow.key + ? (row->key() == _menuRow.key) + : selected), + .paused = videoPaused, + .narrow = (fullWidth < st::columnMinimalWidthLeft), + }); + }; if (_state == WidgetState::Default) { paintCollapsedRows(p, r); const auto &list = _shownList->all(); const auto shownBottom = _shownList->height() - skipTopHeight(); const auto active = activeEntry.key; - const auto selected = _menuRow.key - ? _menuRow.key - : (isPressed() - ? (_pressed - ? _pressed->key() - : Key()) - : (_selected - ? _selected->key() - : Key())); + const auto selected = isPressed() + ? (_pressed ? _pressed->key() : Key()) + : (_selected ? _selected->key() : Key()); if (shownBottom) { const auto skip = dialogsOffset(); const auto promoted = fixedOnTopCount(); @@ -559,23 +580,7 @@ void InnerWidget::paintEvent(QPaintEvent *e) { if (xadd || yadd) { p.translate(xadd, yadd); } - const auto key = row->key(); - const auto isActive = (key == active); - const auto isSelected = (key == selected); - const auto isForum = key.history() - && key.history()->peer->isForum(); - Ui::RowPainter::Paint(p, row, validateVideoUserpic(row), { - .st = (isForum ? &st::forumDialogRow : _st.get()), - .folder = _openedFolder, - .forum = _openedForum, - .filter = _filterId, - .now = ms, - .width = fullWidth, - .active = isActive, - .selected = isSelected, - .paused = videoPaused, - .narrow = (fullWidth < st::columnMinimalWidthLeft), - }); + paintRow(row, (row->key() == selected), true); if (xadd || yadd) { p.translate(-xadd, -yadd); } @@ -677,29 +682,11 @@ void InnerWidget::paintEvent(QPaintEvent *e) { int(_filterResults.size())); p.translate(0, filteredHeight(from)); for (; from < to; ++from) { + const auto selected = isPressed() + ? (from == _filteredPressed) + : (from == _filteredSelected); const auto row = _filterResults[from].row; - const auto key = row->key(); - const auto active = (activeEntry.key == key) - && !activeEntry.fullId; - const auto selected = _menuRow.key - ? (key == _menuRow.key) - : (from == (isPressed() - ? _filteredPressed - : _filteredSelected)); - const auto isForum = key.history() - && key.history()->peer->isForum(); - Ui::RowPainter::Paint(p, row, validateVideoUserpic(row), { - .st = (isForum ? &st::forumDialogRow : _st.get()), - .folder = _openedFolder, - .forum = _openedForum, - .filter = _filterId, - .now = ms, - .width = fullWidth, - .active = active, - .selected = selected, - .paused = videoPaused, - .narrow = (fullWidth < st::columnMinimalWidthLeft), - }); + paintRow(row, selected, !activeEntry.fullId); p.translate(0, row->height()); } } diff --git a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h index f21b8eb278..095d97b717 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h +++ b/Telegram/SourceFiles/dialogs/dialogs_inner_widget.h @@ -49,6 +49,7 @@ namespace Dialogs::Ui { using namespace ::Ui; class VideoUserpic; struct PaintContext; +struct TopicJumpCache; } // namespace Dialogs::Ui namespace Dialogs { @@ -397,6 +398,7 @@ private: std::vector> _collapsedRows; not_null _st; + mutable std::unique_ptr _topicJumpCache; int _collapsedSelected = -1; int _collapsedPressed = -1; bool _skipTopDialog = false; diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.cpp b/Telegram/SourceFiles/dialogs/dialogs_row.cpp index 54ee23524b..aaa6ce7653 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.cpp +++ b/Telegram/SourceFiles/dialogs/dialogs_row.cpp @@ -26,67 +26,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "styles/style_dialogs.h" namespace Dialogs { -namespace { - -[[nodiscard]] TextWithEntities ComposeFolderListEntryText( - not_null folder) { - const auto &list = folder->lastHistories(); - if (list.empty()) { - return {}; - } - - const auto count = std::max( - int(list.size()), - folder->chatsList()->fullSize().current()); - - const auto throwAwayLastName = (list.size() > 1) - && (count == list.size() + 1); - auto &&peers = ranges::views::all( - list - ) | ranges::views::take( - list.size() - (throwAwayLastName ? 1 : 0) - ); - const auto wrapName = [](not_null history) { - const auto name = history->peer->name(); - return TextWithEntities{ - .text = name, - .entities = (history->chatListBadgesState().unread - ? EntitiesInText{ - { EntityType::Semibold, 0, int(name.size()), QString() }, - { EntityType::PlainLink, 0, int(name.size()), QString() }, - } - : EntitiesInText{}), - }; - }; - const auto shown = int(peers.size()); - const auto accumulated = [&] { - Expects(shown > 0); - - auto i = peers.begin(); - auto result = wrapName(*i); - for (++i; i != peers.end(); ++i) { - result = tr::lng_archived_last_list( - tr::now, - lt_accumulated, - result, - lt_chat, - wrapName(*i), - Ui::Text::WithEntities); - } - return result; - }(); - return (shown < count) - ? tr::lng_archived_last( - tr::now, - lt_count, - (count - shown), - lt_chats, - accumulated, - Ui::Text::WithEntities) - : accumulated; -} - -} // namespace BasicRow::BasicRow() = default; BasicRow::~BasicRow() = default; @@ -160,31 +99,13 @@ uint64 Row::sortKey(FilterId filterId) const { return _id.entry()->sortKeyInChatList(filterId); } -void Row::validateListEntryCache() const { - const auto folder = _id.folder(); - if (!folder) { - return; - } - const auto version = folder->chatListViewVersion(); - if (_listEntryCacheVersion == version) { - return; - } - _listEntryCacheVersion = version; - _listEntryCache.setMarkedText( - st::dialogsTextStyle, - ComposeFolderListEntryText(folder), - // Use rich options as long as the entry text does not have user text. - Ui::ItemTextDefaultOptions()); -} - void Row::setCornerBadgeShown( bool shown, Fn updateCallback) const { - const auto value = shown ? 1 : 0; - if (_cornerBadgeShown == value) { + if (_cornerBadgeShown == shown) { return; } - _cornerBadgeShown = value; + const_cast(this)->_cornerBadgeShown = shown; if (_cornerBadgeUserpic && _cornerBadgeUserpic->animation.animating()) { _cornerBadgeUserpic->animation.change( _cornerBadgeShown ? 1. : 0., diff --git a/Telegram/SourceFiles/dialogs/dialogs_row.h b/Telegram/SourceFiles/dialogs/dialogs_row.h index 396042eb7f..74f47e9cb7 100644 --- a/Telegram/SourceFiles/dialogs/dialogs_row.h +++ b/Telegram/SourceFiles/dialogs/dialogs_row.h @@ -119,11 +119,6 @@ public: } [[nodiscard]] uint64 sortKey(FilterId filterId) const; - void validateListEntryCache() const; - [[nodiscard]] const Ui::Text::String &listEntryCache() const { - return _listEntryCache; - } - // for any attached data, for example View in contacts list void *attached = nullptr; @@ -151,13 +146,11 @@ private: const Ui::PaintContext &context); Key _id; - mutable Ui::Text::String _listEntryCache; mutable std::unique_ptr _cornerBadgeUserpic; int _top = 0; int _height = 0; int _index = 0; - mutable int _listEntryCacheVersion : 31 = 0; - mutable int _cornerBadgeShown : 1 = 0; + bool _cornerBadgeShown = false; }; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp index 95ffc9a714..755179b43e 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.cpp @@ -198,22 +198,22 @@ int PaintWideCounter( return availableWidth - used; } -void PaintListEntryText( +void PaintFolderEntryText( Painter &p, - not_null row, + not_null folder, const PaintContext &context, QRect rect) { if (rect.isEmpty()) { return; } - row->validateListEntryCache(); + folder->validateListEntryCache(); p.setFont(st::dialogsTextFont); p.setPen(context.active ? st::dialogsTextFgActive : context.selected ? st::dialogsTextFgOver : st::dialogsTextFg); - row->listEntryCache().draw(p, { + folder->listEntryCache().draw(p, { .position = rect.topLeft(), .availableWidth = rect.width(), .palette = &(context.active @@ -342,7 +342,14 @@ void PaintRow( } } auto texttop = context.st->textTop; - if (promoted && !history->topPromotionMessage().isEmpty()) { + if (const auto folder = entry->asFolder()) { + const auto rect = QRect( + nameleft, + texttop, + namewidth, + st::dialogsTextFont->height); + PaintFolderEntryText(p, folder, context, rect); + } else if (promoted && !history->topPromotionMessage().isEmpty()) { auto availableWidth = namewidth; p.setFont(st::dialogsTextFont); if (history->cloudDraftTextCache().isEmpty()) { @@ -911,21 +918,19 @@ void RowPainter::Paint( : thread ? &thread->lastItemDialogsView() : nullptr; - if (const auto folder = row->folder()) { - PaintListEntryText(p, row, context, rect); - } else if (view) { - if (!view->prepared(item)) { + if (view) { + const auto forum = context.st->topicsHeight + ? row->history()->peer->forum() + : nullptr; + if (!view->prepared(item, forum)) { view->prepare( item, + forum, [=] { entry->updateChatListEntry(); }, - { .ignoreTopic = (!history || !peer->isForum()) }); + {}); } - if (const auto topics = context.st->topicsHeight) { - view->prepareTopics( - row->history()->peer->forum(), - rect, - [=] { entry->updateChatListEntry(); }); - rect.setHeight(topics + rect.height()); + if (forum) { + rect.setHeight(context.st->topicsHeight + rect.height()); } view->paint(p, rect, context); } @@ -1015,10 +1020,10 @@ void RowPainter::Paint( availableWidth, st::dialogsTextFont->height); auto &view = row->itemView(); - if (!view.prepared(item)) { - view.prepare(item, row->repaint(), previewOptions); + if (!view.prepared(item, nullptr)) { + view.prepare(item, nullptr, row->repaint(), previewOptions); } - row->itemView().paint(p, itemRect, context); + view.paint(p, itemRect, context); }; const auto showSavedMessages = history && history->peer->isSelf() diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h index 1968729216..c626161acb 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_layout.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "ui/cached_round_corners.h" + namespace style { struct DialogRow; } // namespace style @@ -35,8 +37,23 @@ using namespace ::Ui; class VideoUserpic; +struct TopicJumpCorners { + Ui::CornersPixmaps normal; + Ui::CornersPixmaps inverted; + QPixmap small; + int invertedRadius = 0; + int smallKey = 0; // = `-radius` if top right else `radius`. +}; + +struct TopicJumpCache { + TopicJumpCorners corners; + TopicJumpCorners over; + TopicJumpCorners rippleMask; +}; + struct PaintContext { not_null st; + TopicJumpCache *topicJumpCache = nullptr; Data::Folder *folder = nullptr; Data::Forum *forum = nullptr; FilterId filter = 0; diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp index 119811d133..ff6bed930e 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.cpp @@ -127,15 +127,32 @@ bool MessageView::dependsOn(not_null item) const { return (_textCachedFor == item.get()); } -bool MessageView::prepared(not_null item) const { - return (_textCachedFor == item.get()); +bool MessageView::prepared( + not_null item, + Data::Forum *forum) const { + return (_textCachedFor == item.get()) + && (!forum + || (_topics + && _topics->forum() == forum + && _topics->prepared())); } void MessageView::prepare( not_null item, + Data::Forum *forum, Fn customEmojiRepaint, ToPreviewOptions options) { - const auto validateTopics = !options.ignoreTopic; + if (!forum) { + _topics = nullptr; + } else if (!_topics || _topics->forum() != forum) { + _topics = std::make_unique(forum); + _topics->prepare(item->topicRootId(), customEmojiRepaint); + } else if (!_topics->prepared()) { + _topics->prepare(item->topicRootId(), customEmojiRepaint); + } + if (_textCachedFor == item.get()) { + return; + } options.existing = &_imagesCache; options.ignoreTopic = true; auto preview = item->toPreview(options); @@ -183,17 +200,21 @@ void MessageView::prepare( } } -void MessageView::prepareTopics( - not_null forum, - const QRect &geometry, - Fn customEmojiRepaint) { - if (!_topics || _topics->forum() != forum) { - _topics = std::make_unique(forum); +int MessageView::countWidth() const { + auto result = 0; + if (!_senderCache.isEmpty()) { + result += _senderCache.maxWidth(); + if (!_imagesCache.empty()) { + result += st::dialogsMiniPreviewSkip + + st::dialogsMiniPreviewRight; + } } - _topics->prepare( - geometry, - &st::forumDialogRow, - std::move(customEmojiRepaint)); + if (!_imagesCache.empty()) { + result += (_imagesCache.size() + * (st::dialogsMiniPreview + st::dialogsMiniPreviewSkip)) + + st::dialogsMiniPreviewRight; + } + return result + _textCache.maxWidth(); } void MessageView::paint( @@ -223,11 +244,22 @@ void MessageView::paint( : st::dialogsTextPalette)); auto rect = geometry; + const auto checkJump = withTopic && !context.active; + const auto jump1 = checkJump ? _topics->jumpToTopicWidth() : 0; + if (jump1) { + paintJumpToLast(p, rect, context, jump1); + } + if (withTopic) { _topics->paint(p, rect, context); rect.setTop(rect.top() + context.st->topicsHeight); } + auto finalRight = rect.x() + rect.width(); + if (jump1) { + rect.setWidth(rect.width() - st::forumDialogJumpArrowSkip); + finalRight -= st::forumDialogJumpArrowSkip; + } const auto lines = rect.height() / st::dialogsTextFont->height; if (!_senderCache.isEmpty()) { _senderCache.draw(p, { @@ -258,20 +290,143 @@ void MessageView::paint( if (!_imagesCache.empty()) { rect.setLeft(rect.x() + st::dialogsMiniPreviewRight); } - if (rect.isEmpty()) { + if (!rect.isEmpty()) { + _textCache.draw(p, { + .position = rect.topLeft(), + .availableWidth = rect.width(), + .palette = palette, + .spoiler = Text::DefaultSpoilerCache(), + .now = context.now, + .paused = context.paused, + .elisionLines = lines, + }); + rect.setLeft(rect.x() + _textCache.maxWidth()); + } + if (jump1) { + const auto x = (rect.width() > st::forumDialogJumpArrowSkip) + ? rect.x() + : finalRight; + const auto add = st::forumDialogJumpArrowLeft; + const auto y = rect.y() + st::forumDialogJumpArrowTop; + (context.selected + ? st::forumDialogJumpArrowOver + : st::forumDialogJumpArrow).paint(p, x + add, y, context.width); + } +} + +void MessageView::paintJumpToLast( + Painter &p, + const QRect &rect, + const PaintContext &context, + int width1) const { + if (!context.topicJumpCache) { return; } - _textCache.draw(p, { - .position = rect.topLeft(), - .availableWidth = rect.width(), - .palette = palette, - .spoiler = Text::DefaultSpoilerCache(), - .now = context.now, - .paused = context.paused, - .elisionLines = lines, + FillJumpToLastBg(p, { + .st = context.st, + .corners = (context.selected + ? &context.topicJumpCache->over + : &context.topicJumpCache->corners), + .geometry = rect, + .bg = (context.selected + ? st::dialogsRippleBg + : st::dialogsBgOver), + .width1 = width1, + .width2 = countWidth() + st::forumDialogJumpArrowSkip, }); } +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()), + }); + 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]); + } + } +} + HistoryView::ItemPreview PreviewWithSender( HistoryView::ItemPreview &&preview, const QString &sender, diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h index f25cce9c04..921d6cb3d2 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_message_view.h @@ -13,6 +13,10 @@ class Image; class HistoryItem; enum class ImageRoundRadius; +namespace style { +struct DialogRow; +} // namespace style + namespace Ui { } // namespace Ui @@ -32,6 +36,7 @@ using namespace ::Ui; struct PaintContext; class TopicsView; +struct TopicJumpCorners; [[nodiscard]] TextWithEntities DialogsPreviewText(TextWithEntities text); @@ -47,17 +52,15 @@ public: void itemInvalidated(not_null item); [[nodiscard]] bool dependsOn(not_null item) const; - [[nodiscard]] bool prepared(not_null item) const; + [[nodiscard]] bool prepared( + not_null item, + Data::Forum *forum) const; void prepare( not_null item, + Data::Forum *forum, Fn customEmojiRepaint, ToPreviewOptions options); - void prepareTopics( - not_null forum, - const QRect &geometry, - Fn customEmojiRepaint); - void paint( Painter &p, const QRect &geometry, @@ -66,6 +69,13 @@ public: private: struct LoadingContext; + [[nodiscard]] int countWidth() const; + void paintJumpToLast( + Painter &p, + const QRect &rect, + const PaintContext &context, + int width1) const; + mutable const HistoryItem *_textCachedFor = nullptr; mutable Text::String _senderCache; mutable std::unique_ptr _topics; @@ -80,4 +90,14 @@ 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 843d0eea6c..f06ba37774 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.cpp @@ -29,47 +29,76 @@ TopicsView::TopicsView(not_null forum) TopicsView::~TopicsView() = default; -void TopicsView::prepare( - const QRect &geometry, - not_null st, - Fn customEmojiRepaint) { +bool TopicsView::prepared() const { + return (_version == _forum->recentTopicsListVersion()); +} + +void TopicsView::prepare(MsgId frontRootId, Fn customEmojiRepaint) { + const auto &list = _forum->recentTopics(); + _version = _forum->recentTopicsListVersion(); + _titles.reserve(list.size()); auto index = 0; - auto available = geometry.width(); - for (const auto &topic : _forum->recentTopics()) { - if (available <= 0) { - break; - } else if (_titles.size() == index) { + for (const auto &topic : list) { + const auto from = begin(_titles) + index; + const auto rootId = topic->rootId(); + const auto i = ranges::find( + from, + end(_titles), + rootId, + &Title::topicRootId); + if (i != end(_titles)) { + if (i != from) { + ranges::rotate(from, i, i + 1); + } + } else if (index >= _titles.size()) { _titles.emplace_back(); } - auto &title = _titles[index]; - const auto rootId = topic->rootId(); + auto &title = _titles[index++]; + title.topicRootId = rootId; + const auto unread = topic->chatListBadgesState().unread; - if (title.topicRootId != rootId || title.unread != unread) { - const auto context = Core::MarkedTextContext{ - .session = &topic->session(), - .customEmojiRepaint = customEmojiRepaint, - .customEmojiLoopLimit = kIconLoopCount, - }; - auto topicTitle = topic->titleWithIcon(); - title.title.setMarkedText( - st::dialogsTextStyle, - (unread - ? Ui::Text::PlainLink( - Ui::Text::Wrapped( - std::move(topicTitle), - EntityType::Bold)) - : std::move(topicTitle)), - DialogTextOptions(), - context); - title.topicRootId = rootId; - title.unread = unread; + if (title.unread == unread + && title.version == topic->titleVersion()) { + continue; } - available -= title.title.maxWidth() + st->topicsSkip; - ++index; + const auto context = Core::MarkedTextContext{ + .session = &topic->session(), + .customEmojiRepaint = customEmojiRepaint, + .customEmojiLoopLimit = kIconLoopCount, + }; + auto topicTitle = topic->titleWithIcon(); + title.version = topic->titleVersion(); + title.unread = unread; + title.title.setMarkedText( + st::dialogsTextStyle, + (unread + ? Ui::Text::PlainLink( + Ui::Text::Wrapped( + std::move(topicTitle), + EntityType::Bold)) + : std::move(topicTitle)), + DialogTextOptions(), + context); } while (_titles.size() > index) { _titles.pop_back(); } + const auto i = frontRootId + ? ranges::find(_titles, frontRootId, &Title::topicRootId) + : end(_titles); + _jumpToTopic = (i != end(_titles)); + if (_jumpToTopic) { + if (i != begin(_titles)) { + ranges::rotate(begin(_titles), i, i + 1); + } + if (!_titles.front().unread) { + _jumpToTopic = false; + } + } +} + +int TopicsView::jumpToTopicWidth() const { + return _jumpToTopic ? _titles.front().title.maxWidth() : 0; } void TopicsView::paint( @@ -91,6 +120,7 @@ void TopicsView::paint( : st::dialogsTextPaletteArchive); auto index = 0; auto rect = geometry; + auto skipBig = _jumpToTopic && !context.active; for (const auto &title : _titles) { if (rect.width() <= 0) { break; @@ -104,8 +134,11 @@ void TopicsView::paint( .paused = context.paused, .elisionLines = 1, }); - rect.setLeft( - rect.left() + title.title.maxWidth() + context.st->topicsSkip); + const auto skip = skipBig + ? context.st->topicsSkipBig + : context.st->topicsSkip; + rect.setLeft(rect.left() + title.title.maxWidth() + skip); + skipBig = false; } } diff --git a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h index 19cff4d658..5a05718fc6 100644 --- a/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h +++ b/Telegram/SourceFiles/dialogs/ui/dialogs_topics_view.h @@ -36,10 +36,10 @@ public: return _forum; } - void prepare( - const QRect &geometry, - not_null st, - Fn customEmojiRepaint); + [[nodiscard]] bool prepared() const; + void prepare(MsgId frontRootId, Fn customEmojiRepaint); + + [[nodiscard]] int jumpToTopicWidth() const; void paint( Painter &p, @@ -54,11 +54,14 @@ private: struct Title { Text::String title; MsgId topicRootId = 0; + int version = -1; bool unread = false; }; const not_null _forum; mutable std::vector _titles; + int _version = -1; + bool _jumpToTopic = false; rpl::lifetime _lifetime; diff --git a/Telegram/SourceFiles/ui/cached_round_corners.cpp b/Telegram/SourceFiles/ui/cached_round_corners.cpp index fc98b3e59c..4aba838513 100644 --- a/Telegram/SourceFiles/ui/cached_round_corners.cpp +++ b/Telegram/SourceFiles/ui/cached_round_corners.cpp @@ -220,7 +220,7 @@ const CornersPixmaps &CachedCornerPixmaps(CachedRoundCorners index) { return Corners[index]; } -CornersPixmaps PrepareCornerPixmaps(int32 radius, style::color bg, const style::color *sh) { +CornersPixmaps PrepareCornerPixmaps(int radius, style::color bg, const style::color *sh) { auto images = PrepareCorners(radius, bg, sh); auto result = CornersPixmaps(); for (int j = 0; j < 4; ++j) { @@ -240,6 +240,24 @@ CornersPixmaps PrepareCornerPixmaps(ImageRoundRadius radius, style::color bg, co Unexpected("Image round radius in PrepareCornerPixmaps."); } +CornersPixmaps PrepareInvertedCornerPixmaps(int radius, style::color bg) { + const auto size = radius * style::DevicePixelRatio(); + auto circle = style::colorizeImage( + style::createInvertedCircleMask(radius * 2), + bg); + circle.setDevicePixelRatio(style::DevicePixelRatio()); + auto result = CornersPixmaps(); + const auto fill = [&](int index, int xoffset, int yoffset) { + result.p[index] = PixmapFromImage( + circle.copy(QRect(xoffset, yoffset, size, size))); + }; + fill(0, 0, 0); + fill(1, size, 0); + fill(2, size, size); + fill(3, 0, size); + return result; +} + [[nodiscard]] int CachedCornerRadiusValue(CachedCornerRadius tag) { using Radius = CachedCornerRadius; switch (tag) { diff --git a/Telegram/SourceFiles/ui/cached_round_corners.h b/Telegram/SourceFiles/ui/cached_round_corners.h index e152e44f9b..f20a06a112 100644 --- a/Telegram/SourceFiles/ui/cached_round_corners.h +++ b/Telegram/SourceFiles/ui/cached_round_corners.h @@ -36,25 +36,28 @@ enum CachedRoundCorners : int { RoundCornersCount }; -void FillRoundRect(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color bg, CachedRoundCorners index); +void FillRoundRect(QPainter &p, int x, int y, int w, int h, style::color bg, CachedRoundCorners index); inline void FillRoundRect(QPainter &p, const QRect &rect, style::color bg, CachedRoundCorners index) { FillRoundRect(p, rect.x(), rect.y(), rect.width(), rect.height(), bg, index); } [[nodiscard]] const CornersPixmaps &CachedCornerPixmaps(CachedRoundCorners index); [[nodiscard]] CornersPixmaps PrepareCornerPixmaps( - int32 radius, + int radius, style::color bg, const style::color *sh = nullptr); [[nodiscard]] CornersPixmaps PrepareCornerPixmaps( ImageRoundRadius radius, style::color bg, const style::color *sh = nullptr); -void FillRoundRect(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color bg, const CornersPixmaps &corners); +[[nodiscard]] CornersPixmaps PrepareInvertedCornerPixmaps( + int radius, + style::color bg); +void FillRoundRect(QPainter &p, int x, int y, int w, int h, style::color bg, const CornersPixmaps &corners); inline void FillRoundRect(QPainter &p, const QRect &rect, style::color bg, const CornersPixmaps &corners) { return FillRoundRect(p, rect.x(), rect.y(), rect.width(), rect.height(), bg, corners); } -void FillRoundShadow(QPainter &p, int32 x, int32 y, int32 w, int32 h, style::color shadow, const CornersPixmaps &corners); +void FillRoundShadow(QPainter &p, int x, int y, int w, int h, style::color shadow, const CornersPixmaps &corners); inline void FillRoundShadow(QPainter &p, const QRect &rect, style::color shadow, const CornersPixmaps &corners) { FillRoundShadow(p, rect.x(), rect.y(), rect.width(), rect.height(), shadow, corners); } diff --git a/Telegram/SourceFiles/ui/chat/chat_style.cpp b/Telegram/SourceFiles/ui/chat/chat_style.cpp index 7b246444f0..9b4eec4de5 100644 --- a/Telegram/SourceFiles/ui/chat/chat_style.cpp +++ b/Telegram/SourceFiles/ui/chat/chat_style.cpp @@ -480,20 +480,9 @@ const CornersPixmaps &ChatStyle::serviceBgCornersNormal() const { const CornersPixmaps &ChatStyle::serviceBgCornersInverted() const { if (_serviceBgCornersInverted.p[0].isNull()) { - const auto radius = HistoryServiceMsgInvertedRadius(); - const auto size = radius * style::DevicePixelRatio(); - auto circle = style::colorizeImage( - style::createInvertedCircleMask(radius * 2), + _serviceBgCornersInverted = Ui::PrepareInvertedCornerPixmaps( + HistoryServiceMsgInvertedRadius(), msgServiceBg()); - circle.setDevicePixelRatio(style::DevicePixelRatio()); - const auto fill = [&](int index, int xoffset, int yoffset) { - _serviceBgCornersInverted.p[index] = PixmapFromImage( - circle.copy(QRect(xoffset, yoffset, size, size))); - }; - fill(0, 0, 0); - fill(1, size, 0); - fill(2, size, size); - fill(3, 0, size); } return _serviceBgCornersInverted; }