From ccc0b22b93dcada8e11f45adb667dbb1acfe5ce4 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 26 May 2026 18:13:22 +0400 Subject: [PATCH] Improve details/summary design. --- Telegram/SourceFiles/history/history_item.cpp | 9 + Telegram/SourceFiles/iv/iv.style | 24 ++- .../iv_markdown_article_layout_blocks.cpp | 4 + .../iv_markdown_article_layout_structure.cpp | 51 ++++-- .../iv/markdown/iv_markdown_article_paint.cpp | 158 ++++++++++++------ 5 files changed, 161 insertions(+), 85 deletions(-) diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 7c39dd920a..a71bed2200 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -688,10 +688,16 @@ HistoryItem::HistoryItem( const auto dropText = fields.ignoreForwardCaptions && _media && (_media->allowsEditCaption() || _media->poll()); + const auto forwardRichPage = dropText + ? std::shared_ptr() + : original->richPage(); const auto forwardText = [&] { if (dropText) { return TextWithEntities(); } + if (forwardRichPage) { + return Iv::FlattenRichPageSummary(forwardRichPage); + } const auto &text = original->originalText(); if (!text.text.isEmpty()) { return dropForwardInfo @@ -703,6 +709,9 @@ HistoryItem::HistoryItem( } return TextWithEntities(); }(); + if (forwardRichPage) { + setRichPage(forwardRichPage); + } setText(forwardText); if (fields.groupedId) { diff --git a/Telegram/SourceFiles/iv/iv.style b/Telegram/SourceFiles/iv/iv.style index 2c9607908f..999aa1a8d3 100644 --- a/Telegram/SourceFiles/iv/iv.style +++ b/Telegram/SourceFiles/iv/iv.style @@ -418,7 +418,7 @@ defaultMarkdownDetails: MarkdownDetails { summaryFg: windowFg; radius: 6px; summaryStyle: defaultMarkdownDetailsSummaryStyle; - icon: icon {{ "history_comments_open", windowSubTextFg }}; + icon: icon {{ "history_down_arrow", windowSubTextFg }}; iconSkip: 8px; } defaultMarkdownEmbedPostAuthorStyle: TextStyle(defaultMarkdownBodyStyle) { @@ -709,10 +709,8 @@ messageMarkdownHeading6Style: TextStyle(messageMarkdownBodyStyle) { font: font(14px semibold); lineHeight: 19px; } -messageMarkdownDetailsSummaryStyle: TextStyle(defaultTextStyle) { - font: font(11px); +messageMarkdownDetailsSummaryStyle: TextStyle(messageMarkdownBodyStyle) { linkUnderline: kLinkUnderlineNever; - lineHeight: 16px; } messageMarkdownAudioTitleStyle: TextStyle(messageMarkdownBodyStyle) { font: font(14px semibold); @@ -739,15 +737,15 @@ messageMarkdownBlockSkips: MarkdownBlockSkips { placeholder: 8px; } messageMarkdownQuoteBgOpacity: 0.12; -messageMarkdownDetailsIcon: icon{{ "history_comments_open", msgInReplyBarColor }}; +messageMarkdownDetailsIcon: icon{{ "history_down_arrow", msgInDateFg }}; messageMarkdownDetailsIconSelected: icon{{ - "history_comments_open", - msgInReplyBarSelColor + "history_down_arrow", + msgInDateFgSelected }}; -messageMarkdownDetailsIconOut: icon{{ "history_comments_open", msgOutReplyBarColor }}; +messageMarkdownDetailsIconOut: icon{{ "history_down_arrow", msgOutDateFg }}; messageMarkdownDetailsIconOutSelected: icon{{ - "history_comments_open", - msgOutReplyBarSelColor + "history_down_arrow", + msgOutDateFgSelected }}; messageMarkdownNavPreviousIcon: icon{{ "box_button_back", msgInDateFg }}; messageMarkdownNavPreviousIconSelected: icon{{ @@ -810,14 +808,14 @@ messageMarkdownDisplayMath: MarkdownDisplayMath(defaultMarkdownDisplayMath) { overflowFg: msgInDateFg; } messageMarkdownDetails: MarkdownDetails(defaultMarkdownDetails) { - headerPadding: margins(6px, 8px, 6px, 8px); - bodyPadding: margins(8px, 8px, 8px, 8px); + headerPadding: margins(4px, 11px, 4px, 11px); + bodyPadding: margins(4px, 11px, 6px, 11px); bodyBg: msgInBg; summaryFg: historyTextInFg; radius: 8px; summaryStyle: messageMarkdownDetailsSummaryStyle; icon: messageMarkdownDetailsIcon; - iconSkip: 6px; + iconSkip: 8px; } messageMarkdownPhoto: MarkdownPhoto(defaultMarkdownPhoto) { captionSkip: 8px; diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp index 0735c5bfe9..5906ad658b 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp @@ -612,6 +612,10 @@ int BlockSkip( const PreparedBlock &block, LayoutContext context, const style::Markdown &st) { + if (previous.kind == PreparedBlockKind::Details + && block.kind == PreparedBlockKind::Details) { + return 0; + } if (context.tightList && IsFlowKind(previous.kind) && IsFlowKind(block.kind)) { diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_structure.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_structure.cpp index 1a0e9709b3..0478a74ef7 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_structure.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_structure.cpp @@ -53,18 +53,18 @@ namespace { case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: + case PreparedBlockKind::Rule: + case PreparedBlockKind::Details: return true; case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::CodeBlock: - case PreparedBlockKind::Rule: case PreparedBlockKind::List: case PreparedBlockKind::ListItem: case PreparedBlockKind::Quote: case PreparedBlockKind::DisplayMath: case PreparedBlockKind::Table: - case PreparedBlockKind::Details: case PreparedBlockKind::Placeholder: case PreparedBlockKind::EmbedPost: return false; @@ -460,18 +460,35 @@ void PrepareNestedContext( block.collapsed = prepared.collapsed; block.supplementary = prepared.supplementary; const auto &details = st.details; + const auto headerPadding = context.useArticleBands + ? QMargins( + st.textPadding.left(), + details.headerPadding.top(), + st.textPadding.right(), + details.headerPadding.bottom()) + : details.headerPadding; + const auto bodyPadding = context.useArticleBands + ? QMargins( + st.textPadding.left(), + details.bodyPadding.top(), + st.textPadding.right(), + details.bodyPadding.bottom()) + : details.bodyPadding; const auto headerWidth = std::max(width, 1); - const auto iconWidth = details.icon.width(); - const auto iconHeight = details.icon.height(); + const auto iconSize = details.icon.empty() + ? 0 + : TextLineHeight(details.summaryStyle); + const auto iconWidth = iconSize; + const auto iconHeight = iconSize; const auto iconSkip = iconWidth ? details.iconSkip : 0; const auto textLeft = left - + details.headerPadding.left() + + headerPadding.left() + iconWidth + iconSkip; block.textWidth = std::max( headerWidth - - details.headerPadding.left() - - details.headerPadding.right() + - headerPadding.left() + - headerPadding.right() - iconWidth - iconSkip, 1); @@ -491,20 +508,20 @@ void PrepareNestedContext( block.leaf.countHeight(block.textWidth, true), TextLineHeight(details.summaryStyle)); const auto headerContentHeight = std::max(summaryHeight, iconHeight); - const auto headerHeight = details.headerPadding.top() + const auto headerHeight = headerPadding.top() + headerContentHeight - + details.headerPadding.bottom(); + + headerPadding.bottom(); block.headerRect = QRect(left, top, headerWidth, headerHeight); if (iconWidth > 0 && iconHeight > 0) { block.iconRect = QRect( - left + details.headerPadding.left(), + left + headerPadding.left(), top + (headerHeight - iconHeight) / 2, iconWidth, iconHeight); } block.textRect = QRect( textLeft, - top + details.headerPadding.top() + top + headerPadding.top() + std::max((headerContentHeight - summaryHeight) / 2, 0), block.textWidth, summaryHeight); @@ -515,12 +532,12 @@ void PrepareNestedContext( auto bottom = top + headerHeight; if (!prepared.collapsed) { - const auto childLeft = left + details.bodyPadding.left(); - const auto childTop = bottom + details.bodyPadding.top(); + const auto childLeft = left + bodyPadding.left(); + const auto childTop = bottom + bodyPadding.top(); const auto childWidth = std::max( headerWidth - - details.bodyPadding.left() - - details.bodyPadding.right(), + - bodyPadding.left() + - bodyPadding.right(), 1); auto childContext = context; PrepareNestedContext(&childContext, childLeft, childWidth); @@ -538,9 +555,9 @@ void PrepareNestedContext( childWidth, childContext); const auto contentHeight = std::max(childBottom - childTop, 0); - const auto bodyHeight = details.bodyPadding.top() + const auto bodyHeight = bodyPadding.top() + contentHeight - + details.bodyPadding.bottom(); + + bodyPadding.bottom(); block.bodyRect = QRect(left, bottom, headerWidth, bodyHeight); block.contentRect = QRect( childLeft, diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp index 6d34ca8d07..555a60d2d1 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_paint.cpp @@ -10,11 +10,13 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "iv/markdown/iv_markdown_article_text.h" #include "ui/dynamic_image.h" #include "ui/effects/path_shift_gradient.h" +#include "ui/effects/toggle_arrow.h" #include "ui/style/style_core_scale.h" #include "ui/widgets/checkbox.h" #include "styles/palette.h" #include "styles/style_iv.h" +#include "styles/style_window.h" #include "styles/style_widgets.h" #include @@ -409,6 +411,83 @@ void FillThinkingGradientImage( return WithOpacity(st.headerBg, st.headerBgOpacity); } +[[nodiscard]] bool UseIncomingMessageDefaultPalette( + const MarkdownArticlePaintContext &context) { + return !context.outbg + && (context.caches.st == &context.messageStyle()->richPageStyle); +} + +[[nodiscard]] QColor ApplySelectedIncomingOverlay( + QColor base, + const MarkdownArticlePaintContext &context) { + if (!context.selected() || context.outbg) { + return base; + } + auto overlay = context.st->msgSelectOverlay()->c; + const auto alpha = overlay.alphaF(); + overlay.setAlpha(base.alpha()); + auto result = anim::color(base, overlay, alpha); + result.setAlpha(base.alpha()); + return result; +} + +[[nodiscard]] QColor EffectiveTableHeaderBg( + const style::Markdown &paintSt, + const MarkdownArticlePaintContext &context) { + return UseIncomingMessageDefaultPalette(context) + ? ApplySelectedIncomingOverlay( + TableHeaderBg(st::defaultMarkdown.table), + context) + : TableHeaderBg(paintSt.table); +} + +[[nodiscard]] QColor EffectiveTableBorderFg( + const style::Markdown &paintSt, + const MarkdownArticlePaintContext &context) { + const auto useIncoming = UseIncomingMessageDefaultPalette(context); + const auto &table = useIncoming + ? st::defaultMarkdown.table + : paintSt.table; + auto result = WithOpacity(table.borderFg, table.headerBgOpacity * 3); + return useIncoming + ? ApplySelectedIncomingOverlay(result, context) + : result; +} + +[[nodiscard]] QColor EffectiveDividerFg( + const style::Markdown &paintSt, + const MarkdownArticlePaintContext &context) { + auto result = UseIncomingMessageDefaultPalette(context) + ? ApplySelectedIncomingOverlay( + st::defaultMarkdown.rule.fg->c, + context) + : paintSt.rule.fg->c; + if (context.outbg) { + result.setAlphaF(result.alphaF() * 0.5); + } + return result; +} + +void PaintDetailsIcon( + Painter &p, + QRect rect, + QColor color, + bool collapsed) { + const auto size = std::min(rect.width(), rect.height()) / 3.; + if (size <= 0.) { + return; + } + const auto center = QRectF(rect).center(); + const auto path = Ui::ToggleUpDownArrowPath( + center.x(), + center.y(), + size, + st::mainMenuToggleFourStrokes, + collapsed ? 0. : 1.); + auto hq = PainterHighQualityEnabler(p); + p.fillPath(path, color); +} + void RefreshBlockThumbnail( const LaidOutBlock &block, const MarkdownArticlePaintContext &context) { @@ -923,6 +1002,8 @@ void PaintWholeTable( const auto radius = st.table.radius; const auto shapePath = TableShapePath(block, border, radius); const auto &paintSt = PaintStyle(context, st); + const auto headerBg = EffectiveTableHeaderBg(paintSt, context); + const auto borderFg = EffectiveTableBorderFg(paintSt, context); p.save(); p.setClipRect(tableClip); @@ -937,7 +1018,7 @@ void PaintWholeTable( if (!cell.header && !striped) { continue; } - p.fillRect(cell.outer, TableHeaderBg(paintSt.table)); + p.fillRect(cell.outer, headerBg); } } p.restore(); @@ -945,11 +1026,7 @@ void PaintWholeTable( if (border > 0 && !block.tableRect.isEmpty()) { const auto path = TableBorderPath(block, border, shapePath); auto hq = PainterHighQualityEnabler(p); - auto pen = QPen( - WithOpacity( - paintSt.table.borderFg, - paintSt.table.headerBgOpacity * 3), - border); + auto pen = QPen(borderFg, border); p.setPen(pen); p.setBrush(Qt::NoBrush); p.drawPath(path); @@ -1025,6 +1102,8 @@ void PaintTableRowBand( const auto radius = st.table.radius; const auto shapePath = TableShapePath(block, border, radius); const auto &paintSt = PaintStyle(context, st); + const auto headerBg = EffectiveTableHeaderBg(paintSt, context); + const auto borderFg = EffectiveTableBorderFg(paintSt, context); const auto cells = TableCellsForRowBand(ownership, rowIndex, rowBand); const auto rowContext = ClippedContext( RevealSuppressedContext(context), @@ -1042,18 +1121,14 @@ void PaintTableRowBand( if (!cell->header && !striped) { continue; } - p.fillRect(cell->outer, TableHeaderBg(paintSt.table)); + p.fillRect(cell->outer, headerBg); } p.restore(); if (border > 0 && !block.tableRect.isEmpty()) { const auto path = TableBorderPath(block, border, shapePath); auto hq = PainterHighQualityEnabler(p); - auto pen = QPen( - WithOpacity( - paintSt.table.borderFg, - paintSt.table.headerBgOpacity * 3), - border); + auto pen = QPen(borderFg, border); p.setPen(pen); p.setBrush(Qt::NoBrush); p.drawPath(path); @@ -1792,54 +1867,27 @@ void PaintDetailsBlock( const auto &details = st.details; const auto &paintSt = PaintStyle(context, st); - const auto &paintDetails = paintSt.details; - const auto headerBg = TableHeaderBg(paintSt.table); - const auto half = details.border / 2.; - const auto outer = QRectF(block.outer).marginsRemoved({ - half, - half, - half, - half, - }); - auto outerPath = QPainterPath(); - outerPath.addRoundedRect(outer, details.radius, details.radius); + const auto lineHeight = details.border; + const auto lineRect = QRect( + block.outer.x(), + block.outer.y() + block.outer.height() - lineHeight, + block.outer.width(), + lineHeight); + const auto lineFg = EffectiveDividerFg(paintSt, context); p.save(); p.setClipRect(visible); - { - auto hq = PainterHighQualityEnabler(p); - p.fillPath( - outerPath, - paintDetails.bodyBg->c); - p.save(); - p.setClipPath(outerPath, Qt::IntersectClip); - p.fillRect(block.headerRect, TableHeaderBg(paintSt.table)); - auto pen = QPen( - WithOpacity( - paintSt.table.borderFg, - paintSt.table.headerBgOpacity * 3), - details.border); - if (!block.bodyRect.isEmpty()) { - p.setPen(pen); - const auto separatorY = block.bodyRect.top() + half; - p.drawLine( - QPointF(outer.left(), separatorY), - QPointF(outer.right(), separatorY)); - } - p.restore(); - - p.setBrush(Qt::NoBrush); - p.setPen(pen); - p.drawPath(outerPath); + if (lineHeight > 0 && !lineRect.isEmpty()) { + p.fillRect(lineRect, lineFg); } if (!block.iconRect.isEmpty()) { - paintDetails.icon.paint( + PaintDetailsIcon( p, - block.iconRect.x(), - block.iconRect.y(), - outerWidth); + block.iconRect, + paintSt.supplementaryTextColor->c, + block.collapsed); } - p.setPen(paintDetails.summaryFg->c); + p.setPen(paintSt.textColor->c); PaintTextLeaf( p, block.leaf, @@ -2020,8 +2068,8 @@ void PaintBlock( p, context, block.outer, - [&](Painter &p, const MarkdownArticlePaintContext &) { - p.fillRect(block.outer, paintSt.rule.fg->c); + [&](Painter &p, const MarkdownArticlePaintContext &context) { + p.fillRect(block.outer, EffectiveDividerFg(paintSt, context)); }); break; case PreparedBlockKind::List: