/* 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 "dialogs/ui/dialogs_topics_view.h" #include "dialogs/ui/dialogs_layout.h" #include "data/data_forum.h" #include "data/data_forum_topic.h" #include "core/ui_integration.h" #include "lang/lang_keys.h" #include "ui/painter.h" #include "ui/power_saving.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 { namespace { constexpr auto kIconLoopCount = 1; } // namespace TopicsView::TopicsView(not_null forum) : _forum(forum) { } TopicsView::~TopicsView() = default; 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; 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 unread = topic->chatListBadgesState().unread; if (title.topicRootId == rootId && title.unread == unread && title.version == topic->titleVersion()) { continue; } const auto context = Core::MarkedTextContext{ .session = &topic->session(), .customEmojiRepaint = customEmojiRepaint, .customEmojiLoopLimit = kIconLoopCount, }; auto topicTitle = topic->titleWithIcon(); title.topicRootId = rootId; title.version = topic->titleVersion(); title.unread = unread; title.title.setMarkedText( st::dialogsTextStyle, (unread ? Ui::Text::Colorized( 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( Painter &p, const QRect &geometry, const PaintContext &context) const { p.setFont(st::dialogsTextFont); p.setPen(context.active ? st::dialogsTextFgActive : context.selected ? st::dialogsTextFgOver : st::dialogsTextFg); const auto palette = &(context.active ? st::dialogsTextPaletteArchiveActive : context.selected ? st::dialogsTextPaletteArchiveOver : st::dialogsTextPaletteArchive); auto rect = geometry; rect.setWidth(rect.width() - _lastTopicJumpGeometry.rightCut); auto skipBig = _jumpToTopic && !context.active; if (_titles.empty()) { p.drawText( rect.x(), rect.y() + st::normalFont->ascent, tr::lng_contacts_loading(tr::now)); return; } for (const auto &title : _titles) { if (rect.width() < title.title.style()->font->elidew) { break; } title.title.draw(p, { .position = rect.topLeft(), .availableWidth = rect.width(), .palette = palette, .spoiler = Text::DefaultSpoilerCache(), .now = context.now, .pausedEmoji = context.paused || On(PowerSaving::kEmojiChat), .pausedSpoiler = context.paused || On(PowerSaving::kChatSpoiler), .elisionOneLine = true, }); const auto skip = skipBig ? context.st->topicsSkipBig : context.st->topicsSkip; rect.setLeft(rect.left() + title.title.maxWidth() + skip); skipBig = false; } } 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::clearRipple() { _ripple = nullptr; } 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 padding = st::forumDialogJumpPadding; const auto availableWidth = context.geometry.width(); const auto want1 = std::min(context.width1, availableWidth); const auto use1 = std::min(want1, availableWidth - padding.right()); const auto use2 = std::min(context.width2, availableWidth); const auto rightCut = want1 - use1; 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{ rightCut, 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{ rightCut, 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