/* 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 "iv/markdown/iv_markdown_article_layout_structure.h" #include "iv/markdown/iv_markdown_article_text.h" #include "lang/lang_keys.h" #include "styles/style_iv.h" #include namespace Iv::Markdown { namespace { [[nodiscard]] int LeafFirstLineBaseline( const Ui::Text::String &leaf, const QRect &textRect, const style::TextStyle &style) { const auto lines = leaf.countLinesGeometry(textRect.width(), true); return textRect.y() + (lines.empty() ? TextLineBaseline(style) : lines.front().baseline); } [[nodiscard]] int MarkdownBodyBaseline( int top, const style::Markdown &st) { return TextLineBaseline(st.body, top); } [[nodiscard]] int BlockBottom(const LaidOutBlock &block) { return block.outer.y() + block.outer.height(); } [[nodiscard]] bool UsesMediaBand(PreparedBlockKind kind) { switch (kind) { case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: 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::List: case PreparedBlockKind::ListItem: case PreparedBlockKind::Quote: case PreparedBlockKind::DisplayMath: case PreparedBlockKind::Table: case PreparedBlockKind::Placeholder: case PreparedBlockKind::EmbedPost: return false; } return false; } [[nodiscard]] QRect PaddedBand( int left, int width, QMargins padding) { return QRect( left + padding.left(), 0, std::max(width - padding.left() - padding.right(), 1), 0); } [[nodiscard]] int PaddedWidth( int width, QMargins padding) { return std::max(width - padding.left() - padding.right(), 1); } [[nodiscard]] int PullquoteIconReserveWidth( const style::QuoteStyle &style) { return style.icon.empty() ? 0 : (style.icon.width() + style.iconPosition.x()); } void RefreshLogicalGeometry(LaidOutBlock *block); [[nodiscard]] int FlowBlockVisibleTextWidth( const LaidOutBlock &block, const EditableMaxLineWidthOverride *override) { if (override && block.editLeaf && (*block.editLeaf == override->leaf)) { return std::min( std::max(override->width, 1), std::max(block.textRect.width(), 1)); } const auto &leaf = block.placeholderLeaf.isEmpty() ? block.leaf : block.placeholderLeaf; return (leaf.isEmpty() || block.textRect.isEmpty()) ? 0 : std::min(leaf.maxWidth(), block.textRect.width()); } [[nodiscard]] bool CanTightenPullquoteChild(const LaidOutBlock &block) { return (block.kind == PreparedBlockKind::Paragraph) && !block.insideHorizontalScroll && (block.horizontalScrollMax <= 0) && block.scrollViewportRect.isEmpty() && block.scrollLogicalContentRect.isEmpty(); } [[nodiscard]] int TightPullquoteTextWidth( const std::vector &children, const EditableMaxLineWidthOverride *override) { auto result = 0; for (const auto &child : children) { if (!CanTightenPullquoteChild(child)) { return 0; } result = std::max(result, FlowBlockVisibleTextWidth(child, override)); } return result; } void RecenterPullquoteChild( LaidOutBlock *block, int left, int width) { if (!block) { return; } block->textWidth = width; block->textRect.setX(left); block->textRect.setWidth(width); block->contentRect.setX(left); block->contentRect.setWidth(width); block->outer.setX(left); block->outer.setWidth(width); block->overflowed = false; block->horizontalScrollMax = 0; block->scrollViewportRect = QRect(); block->scrollLogicalContentRect = QRect(); block->scrollScrollbarTrackRect = QRect(); block->scrollScrollbarThumbRect = QRect(); RefreshLogicalGeometry(block); } [[nodiscard]] QRect BlockBand( PreparedBlockKind kind, const style::Markdown &st, int left, int width, LayoutContext context) { if (!context.useArticleBands) { return QRect(left, 0, std::max(width, 1), 0); } return UsesMediaBand(kind) ? PaddedBand(left, width, st.mediaPadding) : PaddedBand(left, width, st.textPadding); } [[nodiscard]] int BlockBandWidth( PreparedBlockKind kind, const style::Markdown &st, int width, LayoutContext context) { if (!context.useArticleBands) { return std::max(width, 1); } return UsesMediaBand(kind) ? PaddedWidth(width, st.mediaPadding) : PaddedWidth(width, st.textPadding); } [[nodiscard]] bool IsRelatedArticlesHeader( const PreparedBlock &block, const PreparedBlock *next) { return next && (block.kind == PreparedBlockKind::Heading) && (next->kind == PreparedBlockKind::RelatedArticle); } [[nodiscard]] const PreparedBlock *NextVisibleBlock( const std::vector &blocks, int index) { for (auto i = index + 1, count = int(blocks.size()); i != count; ++i) { if (!IsAnchorOnlyBlock(blocks[i])) { return &blocks[i]; } } return nullptr; } void PrepareNestedContext( LayoutContext *context, int left, int width) { context->useArticleBands = false; context->articleLeft = left; context->articleWidth = std::max(width, 1); context->listItemContentShift = 0; } [[nodiscard]] bool TextEmptyForLayout( const PreparedBlock &block, LayoutContext context) { return (block.editLeaf && context.editableTextEmptyOverride && (*block.editLeaf == context.editableTextEmptyOverride->leaf)) ? context.editableTextEmptyOverride->empty : block.text.text.isEmpty(); } [[nodiscard]] bool QuoteBodyEmptyForLayout( const PreparedBlock &block, LayoutContext context) { for (const auto &child : block.children) { if (child.quoteAuthor || IsAnchorOnlyBlock(child)) { continue; } else if (IsFlowKind(child.kind) && TextEmptyForLayout(child, context) && child.children.empty()) { continue; } return false; } return true; } [[nodiscard]] bool HideEmptyQuoteAuthorBlock( const PreparedBlock &block, LayoutContext context) { return context.hideEmptyQuoteAuthor && block.quoteAuthor && TextEmptyForLayout(block, context); } [[nodiscard]] style::align FlowTextAlign(TableAlignment alignment) { switch (alignment) { case TableAlignment::Center: return style::al_center; case TableAlignment::Right: return style::al_right; case TableAlignment::None: case TableAlignment::Left: return style::al_left; } return style::al_left; } [[nodiscard]] LaidOutBlock HiddenQuoteAuthorBlock( const PreparedBlock &prepared) { auto block = LaidOutBlock(); ApplyPreparedEditSources(&block, prepared); block.kind = prepared.kind; block.anchorId = prepared.anchorId; block.anchorIds = prepared.anchorIds; block.headingLevel = prepared.headingLevel; block.supplementary = prepared.supplementary; block.pullquote = prepared.pullquote; block.quoteAuthor = prepared.quoteAuthor; block.flowTextAlign = FlowTextAlign(prepared.flowAlignment); return block; } [[nodiscard]] bool FirstLineComesFromChildren(const LaidOutBlock &block) { switch (block.kind) { case PreparedBlockKind::List: case PreparedBlockKind::ListItem: case PreparedBlockKind::Quote: case PreparedBlockKind::EmbedPost: return true; case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::CodeBlock: case PreparedBlockKind::Rule: case PreparedBlockKind::DisplayMath: case PreparedBlockKind::Table: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::Placeholder: case PreparedBlockKind::Details: return false; } return false; } [[nodiscard]] int ResolveFirstDisplayedLineBaseline( const LaidOutBlock &block, const style::Markdown &st) { if (block.firstLineBaseline >= 0) { return block.firstLineBaseline; } if (FirstLineComesFromChildren(block)) { for (const auto &child : block.children) { if (child.outer.height() <= 0) { continue; } return ResolveFirstDisplayedLineBaseline(child, st); } } return MarkdownBodyBaseline(block.outer.y(), st); } void RefreshLogicalGeometry(LaidOutBlock *block) { block->logicalGeometry = { .outer = block->outer, .headerRect = block->headerRect, .bodyRect = block->bodyRect, .iconRect = block->iconRect, .textRect = block->textRect, .labelRect = block->labelRect, .subtitleRect = block->subtitleRect, .actionRect = block->actionRect, .markerRect = block->markerRect, .contentRect = block->contentRect, .formulaRect = block->formulaRect, .tableRect = block->tableRect, .mediaRect = block->mediaRect, .thumbnailRect = block->thumbnailRect, .markerCenter = block->markerCenter, }; } void ClearBlockGeometry(LaidOutBlock *block) { if (!block) { return; } block->outer = QRect(); block->headerRect = QRect(); block->bodyRect = QRect(); block->iconRect = QRect(); block->textRect = QRect(); block->labelRect = QRect(); block->subtitleRect = QRect(); block->actionRect = QRect(); block->markerRect = QRect(); block->contentRect = QRect(); block->formulaRect = QRect(); block->tableRect = QRect(); block->mediaRect = QRect(); block->thumbnailRect = QRect(); block->visibleFormulaRect = QRect(); block->scrollViewportRect = QRect(); block->scrollLogicalContentRect = QRect(); block->scrollScrollbarTrackRect = QRect(); block->scrollScrollbarThumbRect = QRect(); block->visibleTableRect = QRect(); block->tableScrollbarTrackRect = QRect(); block->tableScrollbarThumbRect = QRect(); block->visibleMediaRect = QRect(); block->markerCenter = QPoint(); block->logicalGeometry = {}; block->textWidth = 0; block->labelWidth = 0; block->subtitleWidth = 0; block->actionWidth = 0; block->markerWidth = 0; block->firstLineBaseline = -1; block->overflowed = false; block->insideHorizontalScroll = false; block->horizontalScrollMax = 0; block->horizontalScrollAncestorShift = 0; } [[nodiscard]] int RetainedLeafHeight( const Ui::Text::String &leaf, const style::TextStyle &textStyle, int width) { return std::max( leaf.countHeight(width, true), TextLineHeight(textStyle)); } [[nodiscard]] bool TextNeedsRetainedLeaf(const QString &text) { for (const auto ch : text) { if (!Ui::Text::IsTrimmed(ch) && !Ui::Text::IsReplacedBySpace(ch)) { return true; } } return false; } [[nodiscard]] bool MissingRetainedLeaf( const QString &text, const Ui::Text::String &leaf) { return TextNeedsRetainedLeaf(text) && leaf.isEmpty(); } struct WidthAnalysisNode { std::vector children; int contentMinimumWidth = 1; int contentPreferredWidth = 1; int outerMinimumWidth = 1; int outerPreferredWidth = 1; int scrollOwnerMinimumWidth = 1; int scrollOwnerOverflowWidth = 1; int scrollViewportMinimumWidth = 1; int outerScrollViewportMinimumWidth = 1; bool ownerEligible = false; bool chosenScrollOwner = false; bool subtreeNeedsScrollOwner = false; bool subtreeHasChildMovingScrollOwner = false; bool subtreeHasScrollOwner = false; }; [[nodiscard]] int MaxChildOuterMinimumWidth( const std::vector &children) { auto result = 1; for (const auto &child : children) { result = std::max(result, child.outerMinimumWidth); } return result; } [[nodiscard]] int MaxChildOuterPreferredWidth( const std::vector &children) { auto result = 1; for (const auto &child : children) { result = std::max(result, child.outerPreferredWidth); } return result; } [[nodiscard]] int MaxUnresolvedChildOuterScrollViewportMinimumWidth( const std::vector &children) { auto result = 1; for (const auto &child : children) { if (child.subtreeNeedsScrollOwner) { result = std::max(result, child.outerScrollViewportMinimumWidth); } } return result; } void FinalizeOwnerSelection( WidthAnalysisNode *analysis, int availableWidth, int visibleViewportWidth, bool ownerMovesChildren) { auto childNeedsScrollOwner = false; auto childHasChildMovingScrollOwner = false; auto childHasScrollOwner = false; for (const auto &child : analysis->children) { childNeedsScrollOwner |= child.subtreeNeedsScrollOwner; childHasChildMovingScrollOwner |= child.subtreeHasChildMovingScrollOwner; childHasScrollOwner |= child.subtreeHasScrollOwner; } const auto nodeOverflows = analysis->scrollOwnerOverflowWidth > std::max(availableWidth, 1); const auto subtreeNeedsScrollOwner = childNeedsScrollOwner || (!childHasChildMovingScrollOwner && nodeOverflows); const auto viewportIsReadable = std::max(visibleViewportWidth, 1) >= std::max(analysis->scrollViewportMinimumWidth, 1); analysis->chosenScrollOwner = subtreeNeedsScrollOwner && analysis->ownerEligible && viewportIsReadable; analysis->subtreeNeedsScrollOwner = subtreeNeedsScrollOwner && !analysis->chosenScrollOwner; analysis->subtreeHasChildMovingScrollOwner = childHasChildMovingScrollOwner || (analysis->chosenScrollOwner && ownerMovesChildren); analysis->subtreeHasScrollOwner = childHasScrollOwner || analysis->chosenScrollOwner; } [[nodiscard]] bool AnalysisOwnerMovesChildren(PreparedBlockKind kind) { switch (kind) { case PreparedBlockKind::List: case PreparedBlockKind::ListItem: case PreparedBlockKind::Quote: case PreparedBlockKind::EmbedPost: case PreparedBlockKind::Details: return true; case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::CodeBlock: case PreparedBlockKind::Rule: case PreparedBlockKind::DisplayMath: case PreparedBlockKind::Table: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::Placeholder: return false; } return false; } [[nodiscard]] int ReadableScrollViewportMinimumWidth( int contentMaxWidth, const style::Markdown &st) { return std::min( std::max(contentMaxWidth, 1), std::max(st.quoteReadableMinWidth, 1)); } [[nodiscard]] int NestedScrollViewportMinimumWidth( LayoutContext context, int contentMaxWidth, const style::Markdown &st) { return (context.quoteDepth > 0 || context.listItemDepth > 0) ? ReadableScrollViewportMinimumWidth(contentMaxWidth, st) : 1; } [[nodiscard]] int OrderedMarkerMinimumWidth( const PreparedBlock &prepared, const style::Markdown &st) { return std::max(st.body.font->width(ListMarkerText(prepared)), 1); } [[nodiscard]] int ListItemMarkerMinimumWidth( const PreparedBlock &prepared, const style::Markdown &st) { if (prepared.taskState != TaskState::None) { return std::max(st.list.taskCheck.diameter, 1); } else if (prepared.listKind == ListKind::Ordered) { return OrderedMarkerMinimumWidth(prepared, st); } return std::max(st.list.markerWidth, 1); } [[nodiscard]] QMargins DetailsHeaderPadding( LayoutContext context, const style::Markdown &st) { const auto &details = st.details; return context.useArticleBands ? QMargins( st.textPadding.left(), details.headerPadding.top(), st.textPadding.right(), details.headerPadding.bottom()) : details.headerPadding; } [[nodiscard]] QString DetailsStateText(bool open) { return open ? tr::lng_iv_details_state_expanded(tr::now) : tr::lng_iv_details_state_collapsed(tr::now); } [[nodiscard]] int DetailsStateReserveWidth(const style::Markdown &st) { return std::max({ st.details.summaryStyle.font->width(DetailsStateText(false)), st.details.summaryStyle.font->width(DetailsStateText(true)), 1, }); } [[nodiscard]] QMargins DetailsBodyPadding( LayoutContext context, const style::Markdown &st) { const auto &details = st.details; return context.useArticleBands ? QMargins( st.textPadding.left(), details.bodyPadding.top(), st.textPadding.right(), details.bodyPadding.bottom()) : details.bodyPadding; } [[nodiscard]] const WidthAnalysisNode *NextActiveScrollOwner( const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner) { return activeScrollOwner ? activeScrollOwner : analysis.chosenScrollOwner ? &analysis : nullptr; } [[nodiscard]] bool IsActiveScrollOwner( const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner) { return (activeScrollOwner == &analysis); } [[nodiscard]] int LogicalOuterWidth( const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, int logicalWidth) { const auto result = std::max(logicalWidth, 1); return IsActiveScrollOwner(analysis, activeScrollOwner) ? std::max(result, analysis.scrollOwnerMinimumWidth) : result; } [[nodiscard]] int ChildLayoutWidth( const WidthAnalysisNode *activeScrollOwner, int visibleWidth, int logicalWidth) { return activeScrollOwner ? std::max(logicalWidth, 1) : std::max(visibleWidth, 1); } [[nodiscard]] int ScrollbarReserveHeight( bool scrollOwner, int horizontalScrollMax, const style::Markdown &st) { return (scrollOwner && (horizontalScrollMax > 0)) ? (st.table.scrollbarSkip + st.table.scrollbarHeight) : 0; } [[nodiscard]] WidthAnalysisNode AnalyzeBlock( const PreparedBlock &prepared, const std::vector &formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &st, int width, LayoutContext context); [[nodiscard]] std::vector AnalyzeBlocks( const std::vector &prepared, const std::vector &formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &st, int width, LayoutContext context) { auto result = std::vector(); result.reserve(prepared.size()); for (auto i = 0, count = int(prepared.size()); i != count; ++i) { const auto &block = prepared[i]; const auto next = NextVisibleBlock(prepared, i); auto blockContext = context; blockContext.preparedPath.push_back(i); auto blockWidth = BlockBandWidth(block.kind, st, width, context); if (IsRelatedArticlesHeader(block, next)) { blockWidth = std::max( width - st.relatedArticle.headerPadding.left() - st.relatedArticle.headerPadding.right(), 1); } result.push_back(AnalyzeBlock( block, formulas, inlineFormulaObjects, mediaRuntime, st, blockWidth, blockContext)); } return result; } [[nodiscard]] WidthAnalysisNode AnalyzeBlock( const PreparedBlock &prepared, const std::vector &formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &st, int width, LayoutContext context) { auto analysis = WidthAnalysisNode(); if (HideEmptyQuoteAuthorBlock(prepared, context)) { return analysis; } const auto availableWidth = std::max(width, 1); auto visibleScrollViewportWidth = availableWidth; switch (prepared.kind) { case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: analysis.contentMinimumWidth = FlowBlockContentMinimumWidth( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, context); analysis.contentPreferredWidth = FlowBlockPreferredWidth( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, context); analysis.outerMinimumWidth = analysis.contentMinimumWidth; analysis.outerPreferredWidth = analysis.contentPreferredWidth; analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth; analysis.scrollViewportMinimumWidth = NestedScrollViewportMinimumWidth( context, analysis.contentPreferredWidth, st); analysis.outerScrollViewportMinimumWidth = std::max( analysis.outerMinimumWidth, analysis.scrollViewportMinimumWidth); analysis.scrollOwnerOverflowWidth = (analysis.scrollViewportMinimumWidth > 1) ? analysis.outerScrollViewportMinimumWidth : analysis.outerMinimumWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerMinimumWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.ownerEligible = !IsAnchorOnlyBlock(prepared); break; case PreparedBlockKind::CodeBlock: { const auto padding = BlockquotePadding(st.code.pre); visibleScrollViewportWidth = availableWidth - HorizontalMarginsWidth(padding); analysis.contentMinimumWidth = CodeBlockMinimumWidth(st); analysis.contentPreferredWidth = CodeBlockPreferredWidth( prepared, st, context); analysis.outerMinimumWidth = analysis.contentMinimumWidth; analysis.outerPreferredWidth = analysis.contentPreferredWidth; analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth; const auto contentMaxWidth = std::max( analysis.contentPreferredWidth - HorizontalMarginsWidth(padding), 1); analysis.scrollViewportMinimumWidth = NestedScrollViewportMinimumWidth( context, contentMaxWidth, st); analysis.outerScrollViewportMinimumWidth = std::max( HorizontalMarginsWidth(padding) + analysis.scrollViewportMinimumWidth, analysis.outerPreferredWidth); analysis.scrollOwnerOverflowWidth = (analysis.scrollViewportMinimumWidth > 1) ? std::max( analysis.outerMinimumWidth, analysis.outerPreferredWidth) : analysis.outerMinimumWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerMinimumWidth, analysis.outerPreferredWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.ownerEligible = true; } break; case PreparedBlockKind::DisplayMath: { const auto &padding = st.displayMath.padding; visibleScrollViewportWidth = availableWidth - HorizontalMarginsWidth(padding); analysis.contentMinimumWidth = DisplayMathMinimumWidth( prepared, formulas, st); analysis.contentPreferredWidth = DisplayMathPreferredWidth( prepared, formulas, st, context); analysis.outerMinimumWidth = analysis.contentMinimumWidth; analysis.outerPreferredWidth = analysis.contentPreferredWidth; analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth; const auto contentMaxWidth = std::max( analysis.contentPreferredWidth - HorizontalMarginsWidth(padding), 1); analysis.scrollViewportMinimumWidth = NestedScrollViewportMinimumWidth( context, contentMaxWidth, st); analysis.outerScrollViewportMinimumWidth = std::max( HorizontalMarginsWidth(padding) + analysis.scrollViewportMinimumWidth, analysis.outerPreferredWidth); analysis.scrollOwnerOverflowWidth = (analysis.scrollViewportMinimumWidth > 1) ? std::max( analysis.outerMinimumWidth, analysis.outerPreferredWidth) : analysis.outerMinimumWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerMinimumWidth, analysis.outerPreferredWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.ownerEligible = true; } break; case PreparedBlockKind::Table: analysis.contentMinimumWidth = TableBlockContentMinimumWidth( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, context); analysis.contentPreferredWidth = analysis.contentMinimumWidth; analysis.outerMinimumWidth = analysis.contentMinimumWidth; analysis.outerPreferredWidth = analysis.contentPreferredWidth; analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth; analysis.scrollViewportMinimumWidth = NestedScrollViewportMinimumWidth( context, analysis.contentPreferredWidth, st); analysis.outerScrollViewportMinimumWidth = std::max( analysis.outerMinimumWidth, analysis.scrollViewportMinimumWidth); analysis.scrollOwnerOverflowWidth = (analysis.scrollViewportMinimumWidth > 1) ? analysis.outerScrollViewportMinimumWidth : analysis.outerMinimumWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerMinimumWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.ownerEligible = true; break; case PreparedBlockKind::List: { const auto depthDelta = std::max( prepared.visualDepth - context.listDepth, 0); const auto markerColumn = context.listItemContentShift; const auto step = std::max(st.textPadding.left(), markerColumn); const auto overhead = depthDelta * step - markerColumn; const auto childWidth = std::max(availableWidth - overhead, 1); visibleScrollViewportWidth = childWidth; auto childContext = context; childContext.listDepth = prepared.visualDepth; childContext.tightList = false; PrepareNestedContext(&childContext, 0, childWidth); analysis.children = AnalyzeBlocks( prepared.children, formulas, inlineFormulaObjects, mediaRuntime, st, childWidth, childContext); const auto childRequiredWidth = MaxUnresolvedChildOuterScrollViewportMinimumWidth( analysis.children); const auto childNeedsScrollOwner = (childRequiredWidth > 1); analysis.contentMinimumWidth = MaxChildOuterMinimumWidth(analysis.children); analysis.contentPreferredWidth = MaxChildOuterPreferredWidth( analysis.children); analysis.outerMinimumWidth = overhead + analysis.contentMinimumWidth; analysis.outerPreferredWidth = overhead + analysis.contentPreferredWidth; const auto naturalOverflowWidth = overhead + analysis.contentMinimumWidth; const auto requiredOverflowWidth = overhead + std::max(analysis.contentMinimumWidth, childRequiredWidth); analysis.scrollViewportMinimumWidth = childNeedsScrollOwner ? ReadableScrollViewportMinimumWidth( analysis.contentPreferredWidth, st) : 1; analysis.outerScrollViewportMinimumWidth = overhead + (childNeedsScrollOwner ? childRequiredWidth : 1); analysis.scrollOwnerOverflowWidth = childNeedsScrollOwner ? requiredOverflowWidth : naturalOverflowWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerOverflowWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.ownerEligible = !prepared.children.empty(); } break; case PreparedBlockKind::ListItem: { const auto markerWidth = std::max( st.list.markerWidth, ListItemMarkerMinimumWidth(prepared, st)); const auto overhead = markerWidth + st.list.markerSkip; const auto childWidth = std::max(availableWidth - overhead, 1); visibleScrollViewportWidth = childWidth; auto childContext = context; childContext.tightList = false; PrepareNestedContext(&childContext, 0, childWidth); childContext.listItemDepth = context.listItemDepth + 1; childContext.listItemContentShift = overhead; analysis.children = AnalyzeBlocks( prepared.children, formulas, inlineFormulaObjects, mediaRuntime, st, childWidth, childContext); const auto childRequiredWidth = MaxUnresolvedChildOuterScrollViewportMinimumWidth( analysis.children); const auto childNeedsScrollOwner = (childRequiredWidth > 1); analysis.contentMinimumWidth = MaxChildOuterMinimumWidth(analysis.children); analysis.contentPreferredWidth = MaxChildOuterPreferredWidth( analysis.children); analysis.outerMinimumWidth = overhead + analysis.contentMinimumWidth; analysis.outerPreferredWidth = overhead + analysis.contentPreferredWidth; const auto naturalOverflowWidth = overhead + analysis.contentMinimumWidth; const auto requiredOverflowWidth = overhead + std::max(analysis.contentMinimumWidth, childRequiredWidth); analysis.scrollViewportMinimumWidth = childNeedsScrollOwner ? ReadableScrollViewportMinimumWidth( analysis.contentPreferredWidth, st) : 1; analysis.outerScrollViewportMinimumWidth = overhead + (childNeedsScrollOwner ? childRequiredWidth : 1); analysis.scrollOwnerOverflowWidth = childNeedsScrollOwner ? requiredOverflowWidth : naturalOverflowWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerOverflowWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.ownerEligible = !prepared.children.empty(); } break; case PreparedBlockKind::Quote: { const auto depthDelta = std::max( prepared.visualDepth - context.quoteDepth, 0); const auto overhead = depthDelta * st.quoteIndent; const auto padding = prepared.pullquote ? st.pullquote.padding : BlockquotePadding(st.body.blockquote); const auto paddingWidth = HorizontalMarginsWidth(padding); const auto pullquoteReserveWidth = prepared.pullquote ? (2 * PullquoteIconReserveWidth(st.body.blockquote)) : 0; auto childWidth = std::max( availableWidth - overhead - paddingWidth - pullquoteReserveWidth, 1); if (prepared.pullquote) { childWidth = std::min(childWidth, std::max(st.pullquote.maxWidth, 1)); } visibleScrollViewportWidth = childWidth; auto childContext = context; childContext.quoteDepth = prepared.visualDepth + 1; childContext.tightList = false; PrepareNestedContext(&childContext, 0, childWidth); childContext.hideEmptyQuoteAuthor = QuoteBodyEmptyForLayout( prepared, childContext); analysis.children = AnalyzeBlocks( prepared.children, formulas, inlineFormulaObjects, mediaRuntime, st, childWidth, childContext); const auto childRequiredWidth = MaxUnresolvedChildOuterScrollViewportMinimumWidth( analysis.children); const auto childNeedsScrollOwner = (childRequiredWidth > 1); analysis.contentMinimumWidth = MaxChildOuterMinimumWidth(analysis.children); analysis.contentPreferredWidth = MaxChildOuterPreferredWidth( analysis.children); const auto visibleContentWidth = prepared.pullquote ? std::min( analysis.contentMinimumWidth, std::max(st.pullquote.maxWidth, 1)) : analysis.contentMinimumWidth; const auto visiblePreferredContentWidth = prepared.pullquote ? std::min( analysis.contentPreferredWidth, std::max(st.pullquote.maxWidth, 1)) : analysis.contentPreferredWidth; analysis.outerMinimumWidth = overhead + paddingWidth + pullquoteReserveWidth + visibleContentWidth; analysis.outerPreferredWidth = overhead + paddingWidth + pullquoteReserveWidth + visiblePreferredContentWidth; if (prepared.pullquote) { const auto requiredContentWidth = std::max( analysis.contentMinimumWidth, childRequiredWidth); analysis.scrollOwnerOverflowWidth = childNeedsScrollOwner ? overhead + paddingWidth + pullquoteReserveWidth + requiredContentWidth : analysis.outerMinimumWidth; analysis.scrollViewportMinimumWidth = childNeedsScrollOwner ? ReadableScrollViewportMinimumWidth( analysis.contentPreferredWidth, st) : 1; analysis.outerScrollViewportMinimumWidth = overhead + paddingWidth + pullquoteReserveWidth + (childNeedsScrollOwner ? childRequiredWidth : 1); analysis.scrollOwnerMinimumWidth = analysis.scrollOwnerOverflowWidth; if (childNeedsScrollOwner) { analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerMinimumWidth, analysis.outerScrollViewportMinimumWidth, }); } } else { const auto requiredContentWidth = std::max( analysis.contentMinimumWidth, childRequiredWidth); analysis.scrollOwnerOverflowWidth = childNeedsScrollOwner ? overhead + paddingWidth + requiredContentWidth : analysis.outerMinimumWidth; analysis.scrollViewportMinimumWidth = childNeedsScrollOwner ? ReadableScrollViewportMinimumWidth( analysis.contentPreferredWidth, st) : 1; analysis.outerScrollViewportMinimumWidth = overhead + paddingWidth + (childNeedsScrollOwner ? childRequiredWidth : 1); analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerOverflowWidth, analysis.outerScrollViewportMinimumWidth, }); } analysis.ownerEligible = !prepared.children.empty(); } break; case PreparedBlockKind::Details: { const auto headerPadding = DetailsHeaderPadding(context, st); const auto bodyPadding = DetailsBodyPadding(context, st); const auto iconWidth = st.details.icon.empty() ? 0 : TextLineHeight(st.details.summaryStyle); const auto iconSkip = iconWidth ? st.details.iconSkip : 0; const auto actionWidth = context.editMode ? DetailsStateReserveWidth(st) : 0; const auto actionSkip = actionWidth ? st.details.stateSkip : 0; const auto headerMinimumWidth = HorizontalMarginsWidth(headerPadding) + iconWidth + iconSkip + actionSkip + actionWidth + DetailsSummaryContentMinimumWidth( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, context); const auto bodyPaddingWidth = HorizontalMarginsWidth(bodyPadding); auto bodyMinimumWidth = 1; auto bodyPreferredWidth = 1; auto childRequiredWidth = 1; auto childNeedsScrollOwner = false; if (!prepared.collapsed) { const auto childWidth = std::max( availableWidth - bodyPaddingWidth, 1); visibleScrollViewportWidth = childWidth; auto childContext = context; PrepareNestedContext(&childContext, 0, childWidth); analysis.children = AnalyzeBlocks( prepared.children, formulas, inlineFormulaObjects, mediaRuntime, st, childWidth, childContext); childRequiredWidth = MaxUnresolvedChildOuterScrollViewportMinimumWidth( analysis.children); childNeedsScrollOwner = (childRequiredWidth > 1); analysis.contentMinimumWidth = MaxChildOuterMinimumWidth( analysis.children); analysis.contentPreferredWidth = MaxChildOuterPreferredWidth( analysis.children); bodyMinimumWidth = ContainerMinimumWidth( analysis.contentMinimumWidth, bodyPadding); bodyPreferredWidth = std::max(analysis.contentPreferredWidth, 1) + bodyPaddingWidth; analysis.scrollViewportMinimumWidth = childNeedsScrollOwner ? ReadableScrollViewportMinimumWidth( analysis.contentPreferredWidth, st) : 1; analysis.outerScrollViewportMinimumWidth = bodyPaddingWidth + (childNeedsScrollOwner ? childRequiredWidth : 1); analysis.scrollOwnerMinimumWidth = std::max({ bodyMinimumWidth, analysis.outerScrollViewportMinimumWidth, }); } analysis.outerMinimumWidth = std::max( headerMinimumWidth, bodyMinimumWidth); analysis.outerPreferredWidth = std::max( headerMinimumWidth, bodyPreferredWidth); analysis.scrollOwnerOverflowWidth = childNeedsScrollOwner ? bodyPaddingWidth + std::max(analysis.contentMinimumWidth, childRequiredWidth) : bodyMinimumWidth; analysis.ownerEligible = !prepared.collapsed && !prepared.children.empty(); } break; case PreparedBlockKind::EmbedPost: { const auto &style = st.embedPost; const auto contentOverhead = style.accentWidth + style.accentSkip + style.padding.left() + style.padding.right(); const auto avatarWidth = prepared.embedPost.authorPhotoId ? std::max(style.avatarSize, 1) : 0; const auto headerGap = avatarWidth ? style.headerGap : 0; auto headerTextWidth = 1; auto headerTextPreferredWidth = 1; if (!prepared.embedPost.author.isEmpty()) { headerTextWidth = std::max( headerTextWidth, ReadableTextMinWidth(style.authorStyle)); headerTextPreferredWidth = std::max( headerTextPreferredWidth, style.authorStyle.font->width(prepared.embedPost.author)); } if (!prepared.embedPost.dateText.isEmpty()) { headerTextWidth = std::max( headerTextWidth, ReadableTextMinWidth(style.dateStyle)); headerTextPreferredWidth = std::max( headerTextPreferredWidth, style.dateStyle.font->width(prepared.embedPost.dateText)); } const auto childWidth = std::max(availableWidth - contentOverhead, 1); visibleScrollViewportWidth = childWidth; auto childContext = context; PrepareNestedContext(&childContext, 0, childWidth); analysis.children = AnalyzeBlocks( prepared.children, formulas, inlineFormulaObjects, mediaRuntime, st, childWidth, childContext); const auto childRequiredWidth = MaxUnresolvedChildOuterScrollViewportMinimumWidth( analysis.children); const auto childNeedsScrollOwner = (childRequiredWidth > 1); analysis.contentMinimumWidth = MaxChildOuterMinimumWidth(analysis.children); analysis.contentPreferredWidth = MaxChildOuterPreferredWidth( analysis.children); const auto naturalOverflowWidth = contentOverhead + analysis.contentMinimumWidth; const auto requiredOverflowWidth = contentOverhead + std::max(analysis.contentMinimumWidth, childRequiredWidth); analysis.scrollViewportMinimumWidth = childNeedsScrollOwner ? ReadableScrollViewportMinimumWidth( analysis.contentPreferredWidth, st) : 1; analysis.outerScrollViewportMinimumWidth = contentOverhead + (childNeedsScrollOwner ? childRequiredWidth : 1); analysis.scrollOwnerOverflowWidth = childNeedsScrollOwner ? requiredOverflowWidth : naturalOverflowWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerOverflowWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.outerMinimumWidth = std::max( contentOverhead + avatarWidth + headerGap + headerTextWidth, analysis.scrollOwnerOverflowWidth); analysis.outerPreferredWidth = std::max( contentOverhead + avatarWidth + headerGap + headerTextPreferredWidth, contentOverhead + analysis.contentPreferredWidth); analysis.ownerEligible = !prepared.children.empty(); } break; case PreparedBlockKind::Rule: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::Placeholder: analysis.contentMinimumWidth = 1; analysis.contentPreferredWidth = analysis.contentMinimumWidth; analysis.outerMinimumWidth = 1; analysis.outerPreferredWidth = analysis.contentPreferredWidth; analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth; analysis.ownerEligible = false; break; } FinalizeOwnerSelection( &analysis, availableWidth, visibleScrollViewportWidth, AnalysisOwnerMovesChildren(prepared.kind)); return analysis; } [[nodiscard]] const Ui::Text::String &DisplayedLeaf( const LaidOutBlock &block) { return block.placeholderLeaf.isEmpty() ? block.leaf : block.placeholderLeaf; } [[nodiscard]] int RetainedLeafMaxWidth( const Ui::Text::String &leaf, int minimum = 1) { return std::max(leaf.maxWidth(), std::max(minimum, 1)); } [[nodiscard]] std::optional AnalyzeRetainedBlock( const PreparedBlock &prepared, const LaidOutBlock &block, const std::vector &formulas, const style::Markdown &st, int width, LayoutContext context); [[nodiscard]] std::optional> AnalyzeRetainedBlocks( const std::vector &prepared, const std::vector &blocks, const std::vector &formulas, const style::Markdown &st, int width, LayoutContext context) { if (prepared.size() != blocks.size()) { return std::nullopt; } auto result = std::vector(); result.reserve(prepared.size()); for (auto i = 0, count = int(prepared.size()); i != count; ++i) { const auto &preparedBlock = prepared[i]; const auto &block = blocks[i]; if (preparedBlock.kind != block.kind) { return std::nullopt; } const auto next = NextVisibleBlock(prepared, i); auto blockContext = context; blockContext.preparedPath.push_back(i); auto blockWidth = BlockBandWidth(preparedBlock.kind, st, width, context); if (IsRelatedArticlesHeader(preparedBlock, next)) { blockWidth = std::max( width - st.relatedArticle.headerPadding.left() - st.relatedArticle.headerPadding.right(), 1); } auto analysis = AnalyzeRetainedBlock( preparedBlock, block, formulas, st, blockWidth, blockContext); if (!analysis) { return std::nullopt; } result.push_back(std::move(*analysis)); } return result; } [[nodiscard]] std::optional AnalyzeRetainedBlock( const PreparedBlock &prepared, const LaidOutBlock &block, const std::vector &formulas, const style::Markdown &st, int width, LayoutContext context) { if (prepared.kind != block.kind) { return std::nullopt; } auto analysis = WidthAnalysisNode(); if (HideEmptyQuoteAuthorBlock(prepared, context)) { return analysis; } const auto availableWidth = std::max(width, 1); auto visibleScrollViewportWidth = availableWidth; switch (prepared.kind) { case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: { const auto &displayLeaf = DisplayedLeaf(block); analysis.contentMinimumWidth = std::max( FlowBlockMinimumWidth(prepared, st), IsAnchorOnlyBlock(prepared) ? 0 : LeafMinimumWidth(displayLeaf)); analysis.contentPreferredWidth = IsAnchorOnlyBlock(prepared) ? 1 : RetainedLeafMaxWidth(displayLeaf); analysis.outerMinimumWidth = analysis.contentMinimumWidth; analysis.outerPreferredWidth = analysis.contentPreferredWidth; analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth; analysis.scrollViewportMinimumWidth = NestedScrollViewportMinimumWidth( context, analysis.contentPreferredWidth, st); analysis.outerScrollViewportMinimumWidth = std::max( analysis.outerMinimumWidth, analysis.scrollViewportMinimumWidth); analysis.scrollOwnerOverflowWidth = (analysis.scrollViewportMinimumWidth > 1) ? analysis.outerScrollViewportMinimumWidth : analysis.outerMinimumWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerMinimumWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.ownerEligible = !IsAnchorOnlyBlock(prepared); } break; case PreparedBlockKind::CodeBlock: { const auto padding = BlockquotePadding(st.code.pre); visibleScrollViewportWidth = availableWidth - HorizontalMarginsWidth(padding); analysis.contentMinimumWidth = CodeBlockMinimumWidth(st); analysis.contentPreferredWidth = HorizontalMarginsWidth(padding) + RetainedLeafMaxWidth(DisplayedLeaf(block)); analysis.outerMinimumWidth = analysis.contentMinimumWidth; analysis.outerPreferredWidth = analysis.contentPreferredWidth; analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth; const auto contentMaxWidth = std::max( analysis.contentPreferredWidth - HorizontalMarginsWidth(padding), 1); analysis.scrollViewportMinimumWidth = NestedScrollViewportMinimumWidth( context, contentMaxWidth, st); analysis.outerScrollViewportMinimumWidth = std::max( HorizontalMarginsWidth(padding) + analysis.scrollViewportMinimumWidth, analysis.outerPreferredWidth); analysis.scrollOwnerOverflowWidth = (analysis.scrollViewportMinimumWidth > 1) ? std::max( analysis.outerMinimumWidth, analysis.outerPreferredWidth) : analysis.outerMinimumWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerMinimumWidth, analysis.outerPreferredWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.ownerEligible = true; } break; case PreparedBlockKind::DisplayMath: { const auto &padding = st.displayMath.padding; visibleScrollViewportWidth = availableWidth - HorizontalMarginsWidth(padding); if (const auto formula = PreparedFormulaFor(formulas, prepared.formulaIndex); formula && formula->measured.success) { analysis.contentMinimumWidth = HorizontalMarginsWidth(padding) + std::max(formula->measured.logicalSize.width(), 1); analysis.contentPreferredWidth = analysis.contentMinimumWidth; } else { const auto &fallbackPadding = st.displayMath.fallbackPadding; const auto &displayLeaf = block.placeholderLeaf.isEmpty() ? block.fallbackLeaf : block.placeholderLeaf; const auto fallbackWidth = RetainedLeafMaxWidth( displayLeaf, ReadableTextMinWidth(st.displayMath.fallbackStyle)); analysis.contentMinimumWidth = HorizontalMarginsWidth(padding) + HorizontalMarginsWidth(fallbackPadding) + fallbackWidth; analysis.contentPreferredWidth = analysis.contentMinimumWidth; } analysis.outerMinimumWidth = analysis.contentMinimumWidth; analysis.outerPreferredWidth = analysis.contentPreferredWidth; analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth; const auto contentMaxWidth = std::max( analysis.contentPreferredWidth - HorizontalMarginsWidth(padding), 1); analysis.scrollViewportMinimumWidth = NestedScrollViewportMinimumWidth( context, contentMaxWidth, st); analysis.outerScrollViewportMinimumWidth = std::max( HorizontalMarginsWidth(padding) + analysis.scrollViewportMinimumWidth, analysis.outerPreferredWidth); analysis.scrollOwnerOverflowWidth = (analysis.scrollViewportMinimumWidth > 1) ? std::max( analysis.outerMinimumWidth, analysis.outerPreferredWidth) : analysis.outerMinimumWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerMinimumWidth, analysis.outerPreferredWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.ownerEligible = true; } break; case PreparedBlockKind::Table: if (prepared.tableRows.size() != block.tableRows.size()) { return std::nullopt; } for (auto rowIndex = 0, rowCount = int(prepared.tableRows.size()); rowIndex != rowCount; ++rowIndex) { if (prepared.tableRows[rowIndex].cells.size() != block.tableRows[rowIndex].cells.size()) { return std::nullopt; } } analysis.contentMinimumWidth = RetainedTableBlockMinimumWidth( prepared, block, st); analysis.contentPreferredWidth = analysis.contentMinimumWidth; analysis.outerMinimumWidth = analysis.contentMinimumWidth; analysis.outerPreferredWidth = analysis.contentPreferredWidth; analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth; analysis.scrollViewportMinimumWidth = NestedScrollViewportMinimumWidth( context, analysis.contentPreferredWidth, st); analysis.outerScrollViewportMinimumWidth = std::max( analysis.scrollViewportMinimumWidth, analysis.outerPreferredWidth); analysis.scrollOwnerOverflowWidth = (analysis.scrollViewportMinimumWidth > 1) ? std::max( analysis.outerMinimumWidth, analysis.outerPreferredWidth) : analysis.outerMinimumWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerMinimumWidth, analysis.outerPreferredWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.ownerEligible = true; break; case PreparedBlockKind::List: { auto childContext = context; const auto depthDelta = std::max( prepared.visualDepth - context.listDepth, 0); const auto markerColumn = context.listItemContentShift; const auto step = std::max(st.textPadding.left(), markerColumn); const auto overhead = depthDelta * step - markerColumn; const auto childWidth = std::max(availableWidth - overhead, 1); visibleScrollViewportWidth = childWidth; childContext.listDepth = prepared.visualDepth; childContext.tightList = false; PrepareNestedContext(&childContext, 0, childWidth); auto children = AnalyzeRetainedBlocks( prepared.children, block.children, formulas, st, childWidth, childContext); if (!children) { return std::nullopt; } analysis.children = std::move(*children); const auto childRequiredWidth = MaxUnresolvedChildOuterScrollViewportMinimumWidth( analysis.children); const auto childNeedsScrollOwner = (childRequiredWidth > 1); analysis.contentMinimumWidth = MaxChildOuterMinimumWidth(analysis.children); analysis.contentPreferredWidth = MaxChildOuterPreferredWidth( analysis.children); analysis.outerMinimumWidth = overhead + analysis.contentMinimumWidth; analysis.outerPreferredWidth = overhead + analysis.contentPreferredWidth; const auto naturalOverflowWidth = overhead + analysis.contentMinimumWidth; const auto requiredOverflowWidth = overhead + std::max(analysis.contentMinimumWidth, childRequiredWidth); analysis.scrollViewportMinimumWidth = childNeedsScrollOwner ? ReadableScrollViewportMinimumWidth( analysis.contentPreferredWidth, st) : 1; analysis.outerScrollViewportMinimumWidth = overhead + (childNeedsScrollOwner ? childRequiredWidth : 1); analysis.scrollOwnerOverflowWidth = childNeedsScrollOwner ? requiredOverflowWidth : naturalOverflowWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerOverflowWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.ownerEligible = !prepared.children.empty(); } break; case PreparedBlockKind::ListItem: { if (prepared.children.size() != block.children.size()) { return std::nullopt; } const auto markerWidth = std::max( st.list.markerWidth, (prepared.taskState != TaskState::None) ? st.list.taskCheck.diameter : (!block.marker.isEmpty() ? RetainedLeafMaxWidth(block.marker) : ListItemMarkerMinimumWidth(prepared, st))); const auto overhead = markerWidth + st.list.markerSkip; const auto childWidth = std::max(availableWidth - overhead, 1); visibleScrollViewportWidth = childWidth; auto childContext = context; childContext.tightList = false; PrepareNestedContext(&childContext, 0, childWidth); childContext.listItemDepth = context.listItemDepth + 1; childContext.listItemContentShift = overhead; auto children = AnalyzeRetainedBlocks( prepared.children, block.children, formulas, st, childWidth, childContext); if (!children) { return std::nullopt; } analysis.children = std::move(*children); const auto childRequiredWidth = MaxUnresolvedChildOuterScrollViewportMinimumWidth( analysis.children); const auto childNeedsScrollOwner = (childRequiredWidth > 1); analysis.contentMinimumWidth = MaxChildOuterMinimumWidth(analysis.children); analysis.contentPreferredWidth = MaxChildOuterPreferredWidth( analysis.children); analysis.outerMinimumWidth = overhead + analysis.contentMinimumWidth; analysis.outerPreferredWidth = overhead + analysis.contentPreferredWidth; const auto naturalOverflowWidth = overhead + analysis.contentMinimumWidth; const auto requiredOverflowWidth = overhead + std::max(analysis.contentMinimumWidth, childRequiredWidth); analysis.scrollViewportMinimumWidth = childNeedsScrollOwner ? ReadableScrollViewportMinimumWidth( analysis.contentPreferredWidth, st) : 1; analysis.outerScrollViewportMinimumWidth = overhead + (childNeedsScrollOwner ? childRequiredWidth : 1); analysis.scrollOwnerOverflowWidth = childNeedsScrollOwner ? requiredOverflowWidth : naturalOverflowWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerOverflowWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.ownerEligible = !prepared.children.empty(); } break; case PreparedBlockKind::Quote: { const auto depthDelta = std::max( prepared.visualDepth - context.quoteDepth, 0); const auto overhead = depthDelta * st.quoteIndent; const auto padding = prepared.pullquote ? st.pullquote.padding : BlockquotePadding(st.body.blockquote); const auto paddingWidth = HorizontalMarginsWidth(padding); const auto pullquoteReserveWidth = prepared.pullquote ? (2 * PullquoteIconReserveWidth(st.body.blockquote)) : 0; auto childWidth = std::max( availableWidth - overhead - paddingWidth - pullquoteReserveWidth, 1); if (prepared.pullquote) { childWidth = std::min(childWidth, std::max(st.pullquote.maxWidth, 1)); } visibleScrollViewportWidth = childWidth; auto childContext = context; childContext.quoteDepth = prepared.visualDepth + 1; childContext.tightList = false; PrepareNestedContext(&childContext, 0, childWidth); childContext.hideEmptyQuoteAuthor = QuoteBodyEmptyForLayout( prepared, childContext); auto children = AnalyzeRetainedBlocks( prepared.children, block.children, formulas, st, childWidth, childContext); if (!children) { return std::nullopt; } analysis.children = std::move(*children); const auto childRequiredWidth = MaxUnresolvedChildOuterScrollViewportMinimumWidth( analysis.children); const auto childNeedsScrollOwner = (childRequiredWidth > 1); analysis.contentMinimumWidth = MaxChildOuterMinimumWidth(analysis.children); analysis.contentPreferredWidth = MaxChildOuterPreferredWidth( analysis.children); const auto visibleContentWidth = prepared.pullquote ? std::min( analysis.contentMinimumWidth, std::max(st.pullquote.maxWidth, 1)) : analysis.contentMinimumWidth; const auto visiblePreferredContentWidth = prepared.pullquote ? std::min( analysis.contentPreferredWidth, std::max(st.pullquote.maxWidth, 1)) : analysis.contentPreferredWidth; analysis.outerMinimumWidth = overhead + paddingWidth + pullquoteReserveWidth + visibleContentWidth; analysis.outerPreferredWidth = overhead + paddingWidth + pullquoteReserveWidth + visiblePreferredContentWidth; if (prepared.pullquote) { const auto requiredContentWidth = std::max( analysis.contentMinimumWidth, childRequiredWidth); analysis.scrollOwnerOverflowWidth = childNeedsScrollOwner ? overhead + paddingWidth + pullquoteReserveWidth + requiredContentWidth : analysis.outerMinimumWidth; analysis.scrollViewportMinimumWidth = childNeedsScrollOwner ? ReadableScrollViewportMinimumWidth( analysis.contentPreferredWidth, st) : 1; analysis.outerScrollViewportMinimumWidth = overhead + paddingWidth + pullquoteReserveWidth + (childNeedsScrollOwner ? childRequiredWidth : 1); analysis.scrollOwnerMinimumWidth = analysis.scrollOwnerOverflowWidth; if (childNeedsScrollOwner) { analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerMinimumWidth, analysis.outerScrollViewportMinimumWidth, }); } } else { const auto requiredContentWidth = std::max( analysis.contentMinimumWidth, childRequiredWidth); analysis.scrollOwnerOverflowWidth = childNeedsScrollOwner ? overhead + paddingWidth + requiredContentWidth : analysis.outerMinimumWidth; analysis.scrollViewportMinimumWidth = childNeedsScrollOwner ? ReadableScrollViewportMinimumWidth( analysis.contentPreferredWidth, st) : 1; analysis.outerScrollViewportMinimumWidth = overhead + paddingWidth + (childNeedsScrollOwner ? childRequiredWidth : 1); analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerOverflowWidth, analysis.outerScrollViewportMinimumWidth, }); } analysis.ownerEligible = !prepared.children.empty(); } break; case PreparedBlockKind::Details: { const auto headerPadding = DetailsHeaderPadding(context, st); const auto bodyPadding = DetailsBodyPadding(context, st); const auto iconWidth = st.details.icon.empty() ? 0 : TextLineHeight(st.details.summaryStyle); const auto iconSkip = iconWidth ? st.details.iconSkip : 0; const auto actionWidth = context.editMode ? DetailsStateReserveWidth(st) : 0; const auto actionSkip = actionWidth ? st.details.stateSkip : 0; const auto summaryMinimumWidth = std::max( ReadableTextMinWidth(st.details.summaryStyle), LeafMinimumWidth(DisplayedLeaf(block))); const auto headerMinimumWidth = HorizontalMarginsWidth(headerPadding) + iconWidth + iconSkip + actionSkip + actionWidth + summaryMinimumWidth; const auto bodyPaddingWidth = HorizontalMarginsWidth(bodyPadding); auto bodyMinimumWidth = 1; auto bodyPreferredWidth = 1; auto childRequiredWidth = 1; auto childNeedsScrollOwner = false; if (prepared.collapsed) { if (!prepared.children.empty() && !block.children.empty()) { return std::nullopt; } } else { const auto childWidth = std::max( availableWidth - bodyPaddingWidth, 1); visibleScrollViewportWidth = childWidth; auto childContext = context; PrepareNestedContext(&childContext, 0, childWidth); auto children = AnalyzeRetainedBlocks( prepared.children, block.children, formulas, st, childWidth, childContext); if (!children) { return std::nullopt; } analysis.children = std::move(*children); childRequiredWidth = MaxUnresolvedChildOuterScrollViewportMinimumWidth( analysis.children); childNeedsScrollOwner = (childRequiredWidth > 1); analysis.contentMinimumWidth = MaxChildOuterMinimumWidth( analysis.children); analysis.contentPreferredWidth = MaxChildOuterPreferredWidth( analysis.children); bodyMinimumWidth = ContainerMinimumWidth( analysis.contentMinimumWidth, bodyPadding); bodyPreferredWidth = std::max(analysis.contentPreferredWidth, 1) + bodyPaddingWidth; analysis.scrollViewportMinimumWidth = childNeedsScrollOwner ? ReadableScrollViewportMinimumWidth( analysis.contentPreferredWidth, st) : 1; analysis.outerScrollViewportMinimumWidth = bodyPaddingWidth + (childNeedsScrollOwner ? childRequiredWidth : 1); analysis.scrollOwnerMinimumWidth = std::max({ bodyMinimumWidth, analysis.outerScrollViewportMinimumWidth, }); } analysis.outerMinimumWidth = std::max( headerMinimumWidth, bodyMinimumWidth); analysis.outerPreferredWidth = std::max( headerMinimumWidth, bodyPreferredWidth); analysis.scrollOwnerOverflowWidth = childNeedsScrollOwner ? bodyPaddingWidth + std::max(analysis.contentMinimumWidth, childRequiredWidth) : bodyMinimumWidth; analysis.ownerEligible = !prepared.collapsed && !prepared.children.empty(); } break; case PreparedBlockKind::EmbedPost: { const auto &style = st.embedPost; const auto contentOverhead = style.accentWidth + style.accentSkip + style.padding.left() + style.padding.right(); const auto avatarWidth = prepared.embedPost.authorPhotoId ? std::max(style.avatarSize, 1) : 0; const auto headerGap = avatarWidth ? style.headerGap : 0; auto headerTextWidth = 1; auto headerTextPreferredWidth = 1; if (!block.labelLeaf.isEmpty()) { headerTextWidth = std::max( headerTextWidth, ReadableTextMinWidth(style.authorStyle)); headerTextPreferredWidth = std::max( headerTextPreferredWidth, RetainedLeafMaxWidth(block.labelLeaf)); } if (!block.subtitleLeaf.isEmpty()) { headerTextWidth = std::max( headerTextWidth, ReadableTextMinWidth(style.dateStyle)); headerTextPreferredWidth = std::max( headerTextPreferredWidth, RetainedLeafMaxWidth(block.subtitleLeaf)); } const auto childWidth = std::max(availableWidth - contentOverhead, 1); visibleScrollViewportWidth = childWidth; auto childContext = context; PrepareNestedContext(&childContext, 0, childWidth); auto children = AnalyzeRetainedBlocks( prepared.children, block.children, formulas, st, childWidth, childContext); if (!children) { return std::nullopt; } analysis.children = std::move(*children); const auto childRequiredWidth = MaxUnresolvedChildOuterScrollViewportMinimumWidth( analysis.children); const auto childNeedsScrollOwner = (childRequiredWidth > 1); analysis.contentMinimumWidth = MaxChildOuterMinimumWidth(analysis.children); analysis.contentPreferredWidth = MaxChildOuterPreferredWidth( analysis.children); const auto naturalOverflowWidth = contentOverhead + analysis.contentMinimumWidth; const auto requiredOverflowWidth = contentOverhead + std::max(analysis.contentMinimumWidth, childRequiredWidth); analysis.scrollViewportMinimumWidth = childNeedsScrollOwner ? ReadableScrollViewportMinimumWidth( analysis.contentPreferredWidth, st) : 1; analysis.outerScrollViewportMinimumWidth = contentOverhead + (childNeedsScrollOwner ? childRequiredWidth : 1); analysis.scrollOwnerOverflowWidth = childNeedsScrollOwner ? requiredOverflowWidth : naturalOverflowWidth; analysis.scrollOwnerMinimumWidth = std::max({ analysis.scrollOwnerOverflowWidth, analysis.outerScrollViewportMinimumWidth, }); analysis.outerMinimumWidth = std::max( contentOverhead + avatarWidth + headerGap + headerTextWidth, analysis.scrollOwnerOverflowWidth); analysis.outerPreferredWidth = std::max( contentOverhead + avatarWidth + headerGap + headerTextPreferredWidth, contentOverhead + analysis.contentPreferredWidth); analysis.ownerEligible = !prepared.children.empty(); } break; case PreparedBlockKind::Rule: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::Placeholder: analysis.contentMinimumWidth = 1; analysis.contentPreferredWidth = analysis.contentMinimumWidth; analysis.outerMinimumWidth = 1; analysis.outerPreferredWidth = analysis.contentPreferredWidth; analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth; analysis.ownerEligible = false; break; } FinalizeOwnerSelection( &analysis, availableWidth, visibleScrollViewportWidth, AnalysisOwnerMovesChildren(prepared.kind)); return analysis; } [[nodiscard]] LaidOutBlock LayoutBlock( const PreparedBlock &prepared, std::vector *formulas, std::vector *renderedFormulas, MathRenderer *renderer, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context); [[nodiscard]] int LayoutBlocks( const std::vector &prepared, std::vector *formulas, std::vector *renderedFormulas, MathRenderer *renderer, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, std::vector *blocks, const std::vector &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context); using LayoutNestedBlocksCallback = std::function( const std::vector &prepared, std::vector *blocks, const std::vector &analysis, const WidthAnalysisNode *activeScrollOwner, int left, int top, int width, int logicalWidth, LayoutContext context)>; using LayoutListChildCallback = std::function( int index, const WidthAnalysisNode *activeScrollOwner, int left, int top, int width, int logicalWidth, LayoutContext context, bool tight)>; [[nodiscard]] LayoutNestedBlocksCallback FreshNestedBlocksCallback( std::vector *formulas, std::vector *renderedFormulas, MathRenderer *renderer, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &st) { return [ formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, &st ](const std::vector &prepared, std::vector *blocks, const std::vector &analysis, const WidthAnalysisNode *activeScrollOwner, int left, int top, int width, int logicalWidth, LayoutContext context) -> std::optional { if (!blocks) { return std::nullopt; } blocks->clear(); return LayoutBlocks( prepared, formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, blocks, analysis, activeScrollOwner, st, left, top, width, logicalWidth, context); }; } [[nodiscard]] std::optional LayoutListItemBlockGeometry( const PreparedBlock &prepared, LaidOutBlock *block, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context, bool tight, const LayoutNestedBlocksCallback &layoutNestedBlocks); [[nodiscard]] std::optional LayoutListBlockGeometry( const PreparedBlock &prepared, LaidOutBlock *block, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context, const LayoutListChildCallback &layoutChild); [[nodiscard]] std::optional LayoutQuoteBlockGeometry( const PreparedBlock &prepared, LaidOutBlock *block, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context, const LayoutNestedBlocksCallback &layoutNestedBlocks); [[nodiscard]] std::optional LayoutDetailsBlockGeometry( const PreparedBlock &prepared, LaidOutBlock *block, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context, const LayoutNestedBlocksCallback &layoutNestedBlocks); [[nodiscard]] std::optional LayoutEmbedPostBlockGeometry( const PreparedBlock &prepared, LaidOutBlock *block, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context, const LayoutNestedBlocksCallback &layoutNestedBlocks); [[nodiscard]] LaidOutBlock LayoutListItemBlock( const PreparedBlock &prepared, std::vector *formulas, std::vector *renderedFormulas, MathRenderer *renderer, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context, bool tight) { auto block = LaidOutBlock(); ApplyPreparedEditSources(&block, prepared); block.kind = PreparedBlockKind::ListItem; block.anchorId = prepared.anchorId; block.anchorIds = prepared.anchorIds; block.supplementary = prepared.supplementary; block.listKind = prepared.listKind; block.listDelimiter = prepared.listDelimiter; block.taskState = prepared.taskState; block.orderedNumber = prepared.orderedNumber; const auto task = (prepared.taskState != TaskState::None); const auto ordered = !task && (prepared.listKind == ListKind::Ordered); const auto markerText = ordered ? ListMarkerText(prepared) : QString(); if (task && prepared.editListItem && context.taskMarkerRippleRuntimeFactory) { block.taskMarkerRippleRuntime = context.taskMarkerRippleRuntimeFactory(*prepared.editListItem); } if (ordered) { BuildOrReusePlainTextLeaf( &block.marker, CachedTextLeafSlot::Marker, prepared, st.body, markerText, PlainTextMinResizeWidth(st.body), context); } const auto bottom = LayoutListItemBlockGeometry( prepared, &block, analysis, activeScrollOwner, st, left, top, width, logicalWidth, context, tight, FreshNestedBlocksCallback( formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, st)); Expects(bottom.has_value()); return block; } [[nodiscard]] LaidOutBlock LayoutListBlock( const PreparedBlock &prepared, std::vector *formulas, std::vector *renderedFormulas, MathRenderer *renderer, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context) { auto block = LaidOutBlock(); ApplyPreparedEditSources(&block, prepared); block.kind = PreparedBlockKind::List; block.anchorId = prepared.anchorId; block.anchorIds = prepared.anchorIds; block.listKind = prepared.listKind; block.listDelimiter = prepared.listDelimiter; const auto layoutChild = [&](int index, const WidthAnalysisNode *childActiveScrollOwner, int childLeft, int childTop, int childWidth, int childLogicalWidth, LayoutContext childContext, bool tight) -> std::optional { const auto &child = prepared.children[index]; auto laidOut = (child.kind == PreparedBlockKind::ListItem) ? LayoutListItemBlock( child, formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, analysis.children[index], childActiveScrollOwner, st, childLeft, childTop, childWidth, childLogicalWidth, childContext, tight) : LayoutBlock( child, formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, analysis.children[index], childActiveScrollOwner, st, childLeft, childTop, childWidth, childLogicalWidth, childContext); const auto bottom = BlockBottom(laidOut); block.children.push_back(std::move(laidOut)); return bottom; }; const auto bottom = LayoutListBlockGeometry( prepared, &block, analysis, activeScrollOwner, st, left, top, width, logicalWidth, context, layoutChild); Expects(bottom.has_value()); return block; } [[nodiscard]] LaidOutBlock LayoutQuoteBlock( const PreparedBlock &prepared, std::vector *formulas, std::vector *renderedFormulas, MathRenderer *renderer, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context) { auto block = LaidOutBlock(); ApplyPreparedEditSources(&block, prepared); block.kind = PreparedBlockKind::Quote; block.anchorId = prepared.anchorId; block.anchorIds = prepared.anchorIds; block.pullquote = prepared.pullquote; const auto bottom = LayoutQuoteBlockGeometry( prepared, &block, analysis, activeScrollOwner, st, left, top, width, logicalWidth, context, FreshNestedBlocksCallback( formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, st)); Expects(bottom.has_value()); return block; } [[nodiscard]] LaidOutBlock LayoutDetailsBlock( const PreparedBlock &prepared, std::vector *formulas, std::vector *renderedFormulas, MathRenderer *renderer, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context) { auto block = LaidOutBlock(); ApplyPreparedEditSources(&block, prepared); block.kind = PreparedBlockKind::Details; block.anchorId = prepared.anchorId; block.anchorIds = prepared.anchorIds; block.collapsed = prepared.collapsed; block.detailsOpen = prepared.detailsOpen; block.supplementary = prepared.supplementary; const auto &details = st.details; BuildOrReuseMarkedTextLeaf( &block.leaf, CachedTextLeafSlot::Leaf, prepared, details.summaryStyle, st, prepared.text, prepared.links, formulas, inlineFormulaObjects, mediaRuntime, FlowTextMinResizeWidth(details.summaryStyle), context); const auto usePlaceholder = prepared.text.text.isEmpty() && !prepared.editPlaceholderText.isEmpty(); if (usePlaceholder) { BuildOrReuseEditPlaceholderLeaf( &block.placeholderText, &block.placeholderLeaf, prepared, prepared.editPlaceholderText, details.summaryStyle, PlainTextMinResizeWidth(details.summaryStyle), context); } if (context.editMode) { BuildOrReusePlainTextLeaf( &block.actionLeaf, CachedTextLeafSlot::Action, prepared, details.summaryStyle, DetailsStateText(prepared.detailsOpen), PlainTextMinResizeWidth(details.summaryStyle), context); } const auto bottom = LayoutDetailsBlockGeometry( prepared, &block, analysis, activeScrollOwner, st, left, top, width, logicalWidth, context, FreshNestedBlocksCallback( formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, st)); Expects(bottom.has_value()); return block; } [[nodiscard]] LaidOutBlock LayoutEmbedPostBlock( const PreparedBlock &prepared, std::vector *formulas, std::vector *renderedFormulas, MathRenderer *renderer, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context) { auto block = LaidOutBlock(); ApplyPreparedEditSources(&block, prepared); block.kind = PreparedBlockKind::EmbedPost; block.anchorId = prepared.anchorId; block.anchorIds = prepared.anchorIds; block.supplementary = prepared.supplementary; block.thumbnailPhotoId = prepared.embedPost.authorPhotoId; if (prepared.embedPost.authorPhotoId && mediaRuntime) { block.photoRuntime = mediaRuntime->resolvePhoto( prepared.embedPost.authorPhotoId); } const auto &style = st.embedPost; if (!prepared.embedPost.author.isEmpty()) { BuildOrReusePlainTextLeaf( &block.labelLeaf, CachedTextLeafSlot::Label, prepared, style.authorStyle, prepared.embedPost.author, PlainTextMinResizeWidth(style.authorStyle), context); } if (!prepared.embedPost.dateText.isEmpty()) { BuildOrReusePlainTextLeaf( &block.subtitleLeaf, CachedTextLeafSlot::Subtitle, prepared, style.dateStyle, prepared.embedPost.dateText, PlainTextMinResizeWidth(style.dateStyle), context); } FillMediaCaptionContent( &block, prepared, formulas, inlineFormulaObjects, mediaRuntime, st, context); const auto bottom = LayoutEmbedPostBlockGeometry( prepared, &block, analysis, activeScrollOwner, st, left, top, width, logicalWidth, context, FreshNestedBlocksCallback( formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, st)); Expects(bottom.has_value()); return block; } [[nodiscard]] LaidOutBlock LayoutBlock( const PreparedBlock &prepared, std::vector *formulas, std::vector *renderedFormulas, MathRenderer *renderer, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context) { const auto currentScrollOwner = NextActiveScrollOwner( analysis, activeScrollOwner); const auto scrollOwner = IsActiveScrollOwner(analysis, currentScrollOwner); const auto effectiveLogicalWidth = LogicalOuterWidth( analysis, currentScrollOwner, logicalWidth); switch (prepared.kind) { case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: return LayoutFlowBlock( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, left, top, width, effectiveLogicalWidth, scrollOwner, context); case PreparedBlockKind::CodeBlock: return LayoutCodeBlock( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, left, top, width, effectiveLogicalWidth, scrollOwner, context.allowAsyncSyntaxHighlighting, context.syntaxHighlightTracker, context); case PreparedBlockKind::Rule: return LayoutRuleBlock(prepared, st, left, top, width); case PreparedBlockKind::List: return LayoutListBlock( prepared, formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, analysis, currentScrollOwner, st, left, top, width, effectiveLogicalWidth, context); case PreparedBlockKind::ListItem: return LayoutListItemBlock( prepared, formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, analysis, currentScrollOwner, st, left, top, width, effectiveLogicalWidth, context, false); case PreparedBlockKind::Quote: return LayoutQuoteBlock( prepared, formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, analysis, currentScrollOwner, st, left, top, width, effectiveLogicalWidth, context); case PreparedBlockKind::DisplayMath: return LayoutDisplayMathBlock( prepared, *formulas, st, left, top, width, effectiveLogicalWidth, scrollOwner, context); case PreparedBlockKind::Table: return LayoutTableBlock( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, left, top, width, effectiveLogicalWidth, scrollOwner, context); case PreparedBlockKind::Photo: return LayoutPhotoBlock( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, left, top, width, context); case PreparedBlockKind::Video: return LayoutVideoBlock( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, left, top, width, context); case PreparedBlockKind::Audio: return LayoutAudioBlock( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, left, top, width, context); case PreparedBlockKind::Map: return LayoutMapBlock( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, left, top, width, context); case PreparedBlockKind::Channel: return LayoutChannelBlock( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, left, top, width, context); case PreparedBlockKind::GroupedMedia: return LayoutGroupedMediaBlock( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, left, top, width, context); case PreparedBlockKind::RelatedArticle: return LayoutRelatedArticleBlock( prepared, st, left, top, width, mediaRuntime, context); case PreparedBlockKind::Placeholder: return LayoutPlaceholderBlock( prepared, formulas, inlineFormulaObjects, mediaRuntime, st, left, top, width, context); case PreparedBlockKind::Details: return LayoutDetailsBlock( prepared, formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, analysis, currentScrollOwner, st, left, top, width, effectiveLogicalWidth, context); case PreparedBlockKind::EmbedPost: return LayoutEmbedPostBlock( prepared, formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, analysis, currentScrollOwner, st, left, top, width, effectiveLogicalWidth, context); } Unexpected("Unknown markdown article block kind."); } int LayoutBlocks( const std::vector &prepared, std::vector *formulas, std::vector *renderedFormulas, MathRenderer *renderer, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, std::vector *blocks, const std::vector &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context) { auto y = top; auto previous = static_cast(nullptr); for (auto i = 0, count = int(prepared.size()); i != count; ++i) { const auto &block = prepared[i]; const auto anchorOnly = IsAnchorOnlyBlock(block); const auto next = NextVisibleBlock(prepared, i); auto blockContext = context; blockContext.preparedPath.push_back(i); if (HideEmptyQuoteAuthorBlock(block, blockContext)) { blocks->push_back(HiddenQuoteAuthorBlock(block)); continue; } if (previous && !anchorOnly) { y += BlockSkip(*previous, block, context, st); } const auto band = BlockBand( block.kind, st, left, std::max(width, 1), context); const auto logicalBandWidth = BlockBandWidth( block.kind, st, logicalWidth, context); auto laidOut = IsRelatedArticlesHeader(block, next) ? LayoutFlowBlock( block, formulas, inlineFormulaObjects, mediaRuntime, st, left + st.relatedArticle.headerPadding.left(), y + st.relatedArticle.headerPadding.top(), std::max( width - st.relatedArticle.headerPadding.left() - st.relatedArticle.headerPadding.right(), 1), std::max( logicalWidth - st.relatedArticle.headerPadding.left() - st.relatedArticle.headerPadding.right(), 1), false, blockContext) : LayoutBlock( block, formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, analysis[i], activeScrollOwner, st, band.x(), y, band.width(), logicalBandWidth, blockContext); if (IsRelatedArticlesHeader(block, next)) { laidOut.headerRect = QRect( left, y, std::max(width, 1), laidOut.outer.height() + st.relatedArticle.headerPadding.top() + st.relatedArticle.headerPadding.bottom()); laidOut.outer = laidOut.headerRect; laidOut.contentRect = laidOut.headerRect; RefreshLogicalGeometry(&laidOut); } y = BlockBottom(laidOut); blocks->push_back(std::move(laidOut)); if (!anchorOnly) { previous = █ } } return y; } [[nodiscard]] std::optional RecountBlockInPlace( const PreparedBlock &prepared, const std::vector &formulas, LaidOutBlock *block, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context); [[nodiscard]] std::optional RecountBlocksInPlace( const std::vector &prepared, const std::vector &formulas, std::vector *blocks, const std::vector &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context); [[nodiscard]] LayoutNestedBlocksCallback RetainedNestedBlocksCallback( const std::vector &formulas, const style::Markdown &st) { return [ &formulas, &st ](const std::vector &prepared, std::vector *blocks, const std::vector &analysis, const WidthAnalysisNode *activeScrollOwner, int left, int top, int width, int logicalWidth, LayoutContext context) -> std::optional { return RecountBlocksInPlace( prepared, formulas, blocks, analysis, activeScrollOwner, st, left, top, width, logicalWidth, context); }; } [[nodiscard]] std::optional LayoutListItemBlockGeometry( const PreparedBlock &prepared, LaidOutBlock *block, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context, bool tight, const LayoutNestedBlocksCallback &layoutNestedBlocks) { if (!block) { return std::nullopt; } const auto &list = st.list; const auto bodyLineHeight = TextLineHeight(st.body); const auto task = (prepared.taskState != TaskState::None); const auto ordered = !task && (prepared.listKind == ListKind::Ordered); auto markerTextWidth = 0; auto markerTextHeight = bodyLineHeight; if (ordered && block->marker.isEmpty()) { return std::nullopt; } if (task) { markerTextWidth = list.taskCheck.diameter; markerTextHeight = list.taskCheck.diameter; } else if (ordered) { markerTextWidth = std::max(block->marker.maxWidth(), 1); markerTextHeight = std::max( block->marker.countHeight(markerTextWidth, true), bodyLineHeight); } ClearBlockGeometry(block); const auto currentScrollOwner = NextActiveScrollOwner( analysis, activeScrollOwner); const auto scrollOwner = IsActiveScrollOwner(analysis, currentScrollOwner); const auto outerLogicalWidth = LogicalOuterWidth( analysis, currentScrollOwner, logicalWidth); block->markerWidth = std::max(list.markerWidth, markerTextWidth); const auto bodyLeft = left + block->markerWidth + list.markerSkip; const auto bodyWidth = std::max( width - block->markerWidth - list.markerSkip, 1); const auto bodyLogicalWidth = std::max( outerLogicalWidth - block->markerWidth - list.markerSkip, 1); const auto childActiveScrollOwner = currentScrollOwner; const auto bodyLayoutWidth = ChildLayoutWidth( childActiveScrollOwner, bodyWidth, bodyLogicalWidth); auto childContext = context; childContext.tightList = tight; PrepareNestedContext(&childContext, bodyLeft, bodyLayoutWidth); childContext.listItemDepth = context.listItemDepth + 1; childContext.listItemContentShift = block->markerWidth + list.markerSkip; const auto childBottom = layoutNestedBlocks( prepared.children, &block->children, analysis.children, childActiveScrollOwner, bodyLeft, top, bodyLayoutWidth, bodyLogicalWidth, childContext); if (!childBottom) { return std::nullopt; } const auto markerBaseline = [&] { for (const auto &child : block->children) { if (child.outer.height() <= 0) { continue; } return ResolveFirstDisplayedLineBaseline(child, st); } return MarkdownBodyBaseline(top, st); }(); const auto contentHeight = *childBottom - top; const auto rowHeight = std::max({ contentHeight, markerTextHeight, bodyLineHeight, }); const auto markerTop = top + std::max( (bodyLineHeight - markerTextHeight) / 2, 0); if (task) { block->markerRect = QRect( left, markerTop, list.taskCheck.diameter, list.taskCheck.diameter); } else if (ordered) { const auto markerLeft = left + block->markerWidth - markerTextWidth; const auto markerLeafBaseline = LeafFirstLineBaseline( block->marker, QRect(0, 0, markerTextWidth, markerTextHeight), st.body); block->markerRect = QRect( markerLeft, markerBaseline - markerLeafBaseline, markerTextWidth, markerTextHeight); } else { block->markerCenter = BulletMarkerCenter(left, markerBaseline, st); } block->contentRect = QRect(bodyLeft, top, bodyWidth, rowHeight); block->horizontalScrollMax = scrollOwner ? std::max(bodyLogicalWidth - bodyWidth, 0) : 0; if (scrollOwner) { block->scrollViewportRect = block->contentRect; block->scrollLogicalContentRect = QRect( bodyLeft, top, bodyLogicalWidth, rowHeight); if (block->horizontalScrollMax > 0) { block->scrollScrollbarTrackRect = QRect( bodyLeft, top + rowHeight + st.table.scrollbarSkip, bodyWidth, st.table.scrollbarHeight); } } block->outer = QRect( left, top, std::max(width, 1), rowHeight + ScrollbarReserveHeight( scrollOwner, block->horizontalScrollMax, st)); block->firstLineBaseline = markerBaseline; RefreshLogicalGeometry(block); return BlockBottom(*block); } [[nodiscard]] std::optional LayoutListBlockGeometry( const PreparedBlock &prepared, LaidOutBlock *block, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context, const LayoutListChildCallback &layoutChild) { if (!block) { return std::nullopt; } ClearBlockGeometry(block); const auto depthDelta = std::max(prepared.visualDepth - context.listDepth, 0); const auto markerColumn = context.listItemContentShift; const auto step = std::max(st.textPadding.left(), markerColumn); const auto indent = depthDelta * step - markerColumn; const auto listLeft = left + indent; const auto listWidth = std::max(width - indent, 1); const auto currentScrollOwner = NextActiveScrollOwner( analysis, activeScrollOwner); const auto scrollOwner = IsActiveScrollOwner(analysis, currentScrollOwner); const auto outerLogicalWidth = LogicalOuterWidth( analysis, currentScrollOwner, logicalWidth); const auto listLogicalWidth = std::max(outerLogicalWidth - indent, 1); const auto childActiveScrollOwner = currentScrollOwner; const auto listLayoutWidth = ChildLayoutWidth( childActiveScrollOwner, listWidth, listLogicalWidth); auto childContext = context; childContext.listDepth = prepared.visualDepth; childContext.tightList = false; PrepareNestedContext(&childContext, listLeft, listLayoutWidth); auto y = top; auto previous = static_cast(nullptr); for (auto i = 0, count = int(prepared.children.size()); i != count; ++i) { const auto &child = prepared.children[i]; const auto anchorOnly = IsAnchorOnlyBlock(child); if (previous && !anchorOnly) { y += prepared.tight ? 0 : BlockSkip(child, st); } const auto childBottom = layoutChild( i, childActiveScrollOwner, listLeft, y, listLayoutWidth, listLogicalWidth, childContext, prepared.tight); if (!childBottom) { return std::nullopt; } y = *childBottom; if (!anchorOnly) { previous = &child; } } block->outer = QRect( listLeft, top, listWidth, std::max(y - top, 0)); block->contentRect = block->outer; block->horizontalScrollMax = scrollOwner ? std::max(listLogicalWidth - listWidth, 0) : 0; if (scrollOwner) { block->scrollViewportRect = block->contentRect; block->scrollLogicalContentRect = QRect( listLeft, top, listLogicalWidth, block->contentRect.height()); if (block->horizontalScrollMax > 0) { block->scrollScrollbarTrackRect = QRect( listLeft, block->contentRect.y() + block->contentRect.height() + st.table.scrollbarSkip, listWidth, st.table.scrollbarHeight); block->outer.setHeight( block->outer.height() + ScrollbarReserveHeight( scrollOwner, block->horizontalScrollMax, st)); } } block->firstLineBaseline = ResolveFirstDisplayedLineBaseline(*block, st); RefreshLogicalGeometry(block); return BlockBottom(*block); } [[nodiscard]] std::optional LayoutQuoteBlockGeometry( const PreparedBlock &prepared, LaidOutBlock *block, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context, const LayoutNestedBlocksCallback &layoutNestedBlocks) { if (!block) { return std::nullopt; } ClearBlockGeometry(block); const auto depthDelta = std::max( prepared.visualDepth - context.quoteDepth, 0); const auto quoteLeft = left + depthDelta * st.quoteIndent; const auto quoteWidth = std::max( width - depthDelta * st.quoteIndent, 1); const auto scrollOwner = IsActiveScrollOwner(analysis, activeScrollOwner); const auto outerLogicalWidth = LogicalOuterWidth( analysis, activeScrollOwner, logicalWidth); const auto quoteLogicalWidth = std::max( outerLogicalWidth - depthDelta * st.quoteIndent, 1); const auto padding = prepared.pullquote ? st.pullquote.padding : BlockquotePadding(st.body.blockquote); const auto pullquoteReserveWidth = prepared.pullquote ? PullquoteIconReserveWidth(st.body.blockquote) : 0; const auto availableWidth = std::max( quoteWidth - padding.left() - padding.right() - 2 * pullquoteReserveWidth, 1); const auto contentWidth = prepared.pullquote ? std::min(availableWidth, std::max(st.pullquote.maxWidth, 1)) : availableWidth; const auto logicalAvailableWidth = std::max( quoteLogicalWidth - padding.left() - padding.right() - 2 * pullquoteReserveWidth, 1); const auto contentLogicalWidth = prepared.pullquote ? std::min(logicalAvailableWidth, std::max(st.pullquote.maxWidth, 1)) : logicalAvailableWidth; const auto contentLeft = prepared.pullquote ? (quoteLeft + padding.left() + pullquoteReserveWidth + ((availableWidth - contentWidth) / 2)) : (quoteLeft + padding.left()); const auto contentTop = top + padding.top(); const auto childActiveScrollOwner = NextActiveScrollOwner( analysis, activeScrollOwner); const auto contentLayoutWidth = ChildLayoutWidth( childActiveScrollOwner, contentWidth, contentLogicalWidth); auto childContext = context; childContext.quoteDepth = prepared.visualDepth + 1; childContext.tightList = false; PrepareNestedContext(&childContext, contentLeft, contentLayoutWidth); childContext.hideEmptyQuoteAuthor = QuoteBodyEmptyForLayout( prepared, childContext); const auto childBottom = layoutNestedBlocks( prepared.children, &block->children, analysis.children, childActiveScrollOwner, contentLeft, contentTop, contentLayoutWidth, contentLogicalWidth, childContext); if (!childBottom) { return std::nullopt; } const auto contentHeight = std::max( *childBottom - contentTop, prepared.children.empty() ? TextLineHeight(st.body) : 0); block->textWidth = contentWidth; block->horizontalScrollMax = scrollOwner ? std::max(contentLogicalWidth - contentWidth, 0) : 0; if (scrollOwner) { block->scrollViewportRect = QRect( contentLeft, contentTop, contentWidth, contentHeight); block->scrollLogicalContentRect = QRect( contentLeft, contentTop, contentLogicalWidth, contentHeight); if (block->horizontalScrollMax > 0) { block->scrollScrollbarTrackRect = QRect( contentLeft, contentTop + contentHeight + st.table.scrollbarSkip, contentWidth, st.table.scrollbarHeight); } } const auto quoteHeight = padding.top() + contentHeight + padding.bottom() + ScrollbarReserveHeight( scrollOwner, block->horizontalScrollMax, st); auto outerLeft = quoteLeft; auto outerWidth = quoteWidth; auto finalContentLeft = contentLeft; auto finalContentWidth = contentWidth; if (prepared.pullquote) { outerWidth = padding.left() + 2 * pullquoteReserveWidth + contentWidth + padding.right(); outerLeft = quoteLeft + ((quoteWidth - outerWidth) / 2); const auto tightTextWidth = TightPullquoteTextWidth( block->children, context.editableMaxLineWidthOverride.get()); if ((tightTextWidth > 0) && (tightTextWidth < contentWidth)) { finalContentWidth = tightTextWidth; finalContentLeft = quoteLeft + padding.left() + pullquoteReserveWidth + ((availableWidth - finalContentWidth) / 2); for (auto &child : block->children) { RecenterPullquoteChild( &child, finalContentLeft, finalContentWidth); } outerWidth = padding.left() + 2 * pullquoteReserveWidth + finalContentWidth + padding.right(); outerLeft = quoteLeft + ((quoteWidth - outerWidth) / 2); } } block->outer = QRect(outerLeft, top, outerWidth, quoteHeight); block->contentRect = QRect( finalContentLeft, contentTop, finalContentWidth, contentHeight); block->firstLineBaseline = ResolveFirstDisplayedLineBaseline(*block, st); RefreshLogicalGeometry(block); return BlockBottom(*block); } [[nodiscard]] std::optional LayoutDetailsBlockGeometry( const PreparedBlock &prepared, LaidOutBlock *block, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context, const LayoutNestedBlocksCallback &layoutNestedBlocks) { if (!block) { return std::nullopt; } const auto usePlaceholder = prepared.text.text.isEmpty() && !prepared.editPlaceholderText.isEmpty(); if (MissingRetainedLeaf(prepared.text.text, block->leaf) || (usePlaceholder && block->placeholderLeaf.isEmpty())) { return std::nullopt; } const auto actionWidth = context.editMode ? DetailsStateReserveWidth(st) : 0; if (actionWidth > 0 && block->actionLeaf.isEmpty()) { return std::nullopt; } ClearBlockGeometry(block); const auto &details = st.details; const auto headerPadding = DetailsHeaderPadding(context, st); const auto bodyPadding = DetailsBodyPadding(context, st); const auto headerWidth = std::max(width, 1); const auto scrollOwner = IsActiveScrollOwner(analysis, activeScrollOwner); const auto outerLogicalWidth = LogicalOuterWidth( analysis, activeScrollOwner, logicalWidth); const auto iconWidth = details.icon.empty() ? 0 : TextLineHeight(details.summaryStyle); const auto iconHeight = iconWidth; const auto iconSkip = iconWidth ? details.iconSkip : 0; const auto actionSkip = actionWidth ? details.stateSkip : 0; const auto actionZoneWidth = actionSkip + actionWidth; const auto textLeft = left + headerPadding.left() + iconWidth + iconSkip; block->textWidth = std::max( headerWidth - headerPadding.left() - headerPadding.right() - iconWidth - iconSkip - actionZoneWidth, 1); const auto &displayLeaf = usePlaceholder ? block->placeholderLeaf : block->leaf; const auto summaryHeight = ResolveEditableHeight( RetainedLeafHeight( displayLeaf, details.summaryStyle, block->textWidth), context); auto actionHeight = 0; if (actionWidth > 0) { block->actionWidth = actionWidth; actionHeight = RetainedLeafHeight( block->actionLeaf, details.summaryStyle, block->actionWidth); } const auto headerContentHeight = std::max(summaryHeight, iconHeight); const auto headerHeight = headerPadding.top() + headerContentHeight + headerPadding.bottom(); block->headerRect = QRect(left, top, headerWidth, headerHeight); if (iconWidth > 0 && iconHeight > 0) { block->iconRect = QRect( left + headerPadding.left(), top + (headerHeight - iconHeight) / 2, iconWidth, iconHeight); } block->textRect = QRect( textLeft, top + headerPadding.top() + std::max((headerContentHeight - summaryHeight) / 2, 0), block->textWidth, summaryHeight); if (actionZoneWidth > 0 && actionHeight > 0) { block->actionRect = QRect( left + headerWidth - headerPadding.right() - actionZoneWidth, top + headerPadding.top() + std::max((headerContentHeight - actionHeight) / 2, 0), actionZoneWidth, actionHeight); } block->firstLineBaseline = LeafFirstLineBaseline( displayLeaf, block->textRect, details.summaryStyle); const auto childActiveScrollOwner = NextActiveScrollOwner( analysis, activeScrollOwner); auto bottom = top + headerHeight; if (!prepared.collapsed) { const auto childLeft = left + bodyPadding.left(); const auto childTop = bottom + bodyPadding.top(); const auto childWidth = std::max( headerWidth - bodyPadding.left() - bodyPadding.right(), 1); const auto childLogicalWidth = std::max( outerLogicalWidth - bodyPadding.left() - bodyPadding.right(), 1); const auto childLayoutWidth = ChildLayoutWidth( childActiveScrollOwner, childWidth, childLogicalWidth); auto childContext = context; PrepareNestedContext(&childContext, childLeft, childLayoutWidth); const auto childBottom = layoutNestedBlocks( prepared.children, &block->children, analysis.children, childActiveScrollOwner, childLeft, childTop, childLayoutWidth, childLogicalWidth, childContext); if (!childBottom) { return std::nullopt; } const auto contentHeight = std::max(*childBottom - childTop, 0); const auto bodyHeight = bodyPadding.top() + contentHeight + bodyPadding.bottom(); block->bodyRect = QRect(left, bottom, headerWidth, bodyHeight); block->contentRect = QRect( childLeft, childTop, childWidth, contentHeight); block->horizontalScrollMax = scrollOwner ? std::max(childLogicalWidth - childWidth, 0) : 0; if (scrollOwner) { block->scrollViewportRect = block->contentRect; block->scrollLogicalContentRect = QRect( childLeft, childTop, childLogicalWidth, contentHeight); if (block->horizontalScrollMax > 0) { block->scrollScrollbarTrackRect = QRect( childLeft, childTop + contentHeight + st.table.scrollbarSkip, childWidth, st.table.scrollbarHeight); } } bottom += bodyHeight + ScrollbarReserveHeight( scrollOwner, block->horizontalScrollMax, st); } block->outer = QRect( left, top, headerWidth, std::max(bottom - top, headerHeight)); RefreshLogicalGeometry(block); return BlockBottom(*block); } [[nodiscard]] std::optional LayoutEmbedPostBlockGeometry( const PreparedBlock &prepared, LaidOutBlock *block, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context, const LayoutNestedBlocksCallback &layoutNestedBlocks) { if (!block || MissingRetainedLeaf(prepared.embedPost.author, block->labelLeaf) || MissingRetainedLeaf(prepared.embedPost.dateText, block->subtitleLeaf) || (prepared.embedPost.authorPhotoId && !block->photoRuntime)) { return std::nullopt; } ClearBlockGeometry(block); const auto &style = st.embedPost; const auto blockWidth = std::max(width, 1); const auto scrollOwner = IsActiveScrollOwner(analysis, activeScrollOwner); const auto outerLogicalWidth = LogicalOuterWidth( analysis, activeScrollOwner, logicalWidth); const auto contentLeft = left + style.accentWidth + style.accentSkip + style.padding.left(); const auto contentTop = top + style.padding.top(); const auto contentWidth = std::max( blockWidth - style.accentWidth - style.accentSkip - style.padding.left() - style.padding.right(), 1); const auto contentLogicalWidth = std::max( outerLogicalWidth - style.accentWidth - style.accentSkip - style.padding.left() - style.padding.right(), 1); const auto hasAvatar = (block->photoRuntime != nullptr); const auto avatarSize = hasAvatar ? std::max(style.avatarSize, 1) : 0; const auto headerGap = hasAvatar ? style.headerGap : 0; const auto textLeft = contentLeft + avatarSize + headerGap; const auto textWidth = std::max(contentWidth - avatarSize - headerGap, 1); const auto childActiveScrollOwner = NextActiveScrollOwner( analysis, activeScrollOwner); auto authorHeight = 0; if (!prepared.embedPost.author.isEmpty()) { block->labelWidth = textWidth; authorHeight = ResolveEditableHeight( RetainedLeafHeight( block->labelLeaf, style.authorStyle, textWidth), context); } auto dateHeight = 0; if (!prepared.embedPost.dateText.isEmpty()) { block->subtitleWidth = textWidth; dateHeight = ResolveEditableHeight( RetainedLeafHeight( block->subtitleLeaf, style.dateStyle, textWidth), context); } const auto textHeight = authorHeight + dateHeight; const auto headerHeight = std::max(textHeight, avatarSize); if (headerHeight > 0) { block->headerRect = QRect(contentLeft, contentTop, contentWidth, headerHeight); } if (avatarSize > 0) { block->thumbnailRect = QRect( contentLeft, contentTop + std::max((headerHeight - avatarSize) / 2, 0), avatarSize, avatarSize); } auto textTop = contentTop + std::max((headerHeight - textHeight) / 2, 0); if (authorHeight > 0) { block->labelRect = QRect(textLeft, textTop, textWidth, authorHeight); textTop += authorHeight; } if (dateHeight > 0) { block->subtitleRect = QRect(textLeft, textTop, textWidth, dateHeight); } auto wrapperBottom = contentTop + headerHeight; if (!prepared.children.empty()) { const auto bodyTop = wrapperBottom + ((headerHeight > 0) ? style.bodySkip : 0); const auto contentLayoutWidth = ChildLayoutWidth( childActiveScrollOwner, contentWidth, contentLogicalWidth); auto childContext = context; PrepareNestedContext(&childContext, contentLeft, contentLayoutWidth); const auto childBottom = layoutNestedBlocks( prepared.children, &block->children, analysis.children, childActiveScrollOwner, contentLeft, bodyTop, contentLayoutWidth, contentLogicalWidth, childContext); if (!childBottom) { return std::nullopt; } const auto bodyHeight = std::max(*childBottom - bodyTop, 0); block->bodyRect = QRect(contentLeft, bodyTop, contentWidth, bodyHeight); wrapperBottom = std::max(wrapperBottom, *childBottom); block->horizontalScrollMax = scrollOwner ? std::max(contentLogicalWidth - contentWidth, 0) : 0; if (scrollOwner) { block->scrollViewportRect = block->bodyRect; block->scrollLogicalContentRect = QRect( contentLeft, bodyTop, contentLogicalWidth, bodyHeight); if (block->horizontalScrollMax > 0) { block->scrollScrollbarTrackRect = QRect( contentLeft, bodyTop + bodyHeight + st.table.scrollbarSkip, contentWidth, st.table.scrollbarHeight); wrapperBottom = std::max( wrapperBottom, block->scrollScrollbarTrackRect.y() + block->scrollScrollbarTrackRect.height()); } } } block->contentRect = QRect( contentLeft, contentTop, contentWidth, std::max(wrapperBottom - contentTop, 0)); block->mediaRect = QRect( left, top, blockWidth, style.padding.top() + block->contentRect.height() + style.padding.bottom()); auto bottom = block->mediaRect.y() + block->mediaRect.height(); if (!LayoutMediaCaptionGeometry( block, prepared, st, left, bottom, blockWidth, style.captionSkip, &bottom, context)) { return std::nullopt; } block->outer = QRect( left, top, blockWidth, std::max(bottom - top, block->mediaRect.height())); if (!block->labelRect.isEmpty() && !block->labelLeaf.isEmpty()) { block->firstLineBaseline = LeafFirstLineBaseline( block->labelLeaf, block->labelRect, style.authorStyle); } else if (!block->subtitleRect.isEmpty() && !block->subtitleLeaf.isEmpty()) { block->firstLineBaseline = LeafFirstLineBaseline( block->subtitleLeaf, block->subtitleRect, style.dateStyle); } else { for (const auto &child : block->children) { if (child.outer.height() <= 0) { continue; } block->firstLineBaseline = ResolveFirstDisplayedLineBaseline( child, st); break; } if (block->firstLineBaseline < 0) { block->firstLineBaseline = MarkdownBodyBaseline(top, st); } } RefreshLogicalGeometry(block); return BlockBottom(*block); } [[nodiscard]] std::optional RecountBlockInPlace( const PreparedBlock &prepared, const std::vector &formulas, LaidOutBlock *block, const WidthAnalysisNode &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context) { const auto currentScrollOwner = NextActiveScrollOwner( analysis, activeScrollOwner); const auto scrollOwner = IsActiveScrollOwner(analysis, currentScrollOwner); const auto effectiveLogicalWidth = LogicalOuterWidth( analysis, currentScrollOwner, logicalWidth); switch (prepared.kind) { case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: case PreparedBlockKind::CodeBlock: case PreparedBlockKind::Rule: case PreparedBlockKind::DisplayMath: case PreparedBlockKind::Table: case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::Placeholder: return RecountSimpleLaidOutBlock( prepared, formulas, block, st, left, top, width, effectiveLogicalWidth, scrollOwner, context); case PreparedBlockKind::List: return LayoutListBlockGeometry( prepared, block, analysis, currentScrollOwner, st, left, top, width, effectiveLogicalWidth, context, [&](int index, const WidthAnalysisNode *childActiveScrollOwner, int childLeft, int childTop, int childWidth, int childLogicalWidth, LayoutContext childContext, bool tight) -> std::optional { const auto &child = prepared.children[index]; return (child.kind == PreparedBlockKind::ListItem) ? LayoutListItemBlockGeometry( child, &block->children[index], analysis.children[index], childActiveScrollOwner, st, childLeft, childTop, childWidth, childLogicalWidth, childContext, tight, RetainedNestedBlocksCallback(formulas, st)) : RecountBlockInPlace( child, formulas, &block->children[index], analysis.children[index], childActiveScrollOwner, st, childLeft, childTop, childWidth, childLogicalWidth, childContext); }); case PreparedBlockKind::ListItem: return LayoutListItemBlockGeometry( prepared, block, analysis, currentScrollOwner, st, left, top, width, effectiveLogicalWidth, context, false, RetainedNestedBlocksCallback(formulas, st)); case PreparedBlockKind::Quote: return LayoutQuoteBlockGeometry( prepared, block, analysis, currentScrollOwner, st, left, top, width, effectiveLogicalWidth, context, RetainedNestedBlocksCallback(formulas, st)); case PreparedBlockKind::Details: return LayoutDetailsBlockGeometry( prepared, block, analysis, currentScrollOwner, st, left, top, width, effectiveLogicalWidth, context, RetainedNestedBlocksCallback(formulas, st)); case PreparedBlockKind::EmbedPost: return LayoutEmbedPostBlockGeometry( prepared, block, analysis, currentScrollOwner, st, left, top, width, effectiveLogicalWidth, context, RetainedNestedBlocksCallback(formulas, st)); } return std::nullopt; } [[nodiscard]] const style::TextStyle &LaidOutFlowTextStyle( const LaidOutBlock &block, const style::Markdown &st) { if (block.kind != PreparedBlockKind::Heading) { return st.body; } switch (std::clamp(block.headingLevel, 1, 6)) { case 1: return st.heading1; case 2: return st.heading2; case 3: return st.heading3; case 4: return st.heading4; case 5: return st.heading5; case 6: return st.heading6; } return st.heading6; } [[nodiscard]] int BlockContentMaxRight( const LaidOutBlock &block, const style::Markdown &st) { const auto outerRight = block.outer.x() + block.outer.width(); const auto textRight = [&] { const auto &leaf = block.placeholderLeaf.isEmpty() ? block.leaf : block.placeholderLeaf; return (leaf.isEmpty() || block.textRect.isEmpty()) ? 0 : (block.textRect.x() + std::min(leaf.maxWidth(), block.textRect.width())); }; const auto childrenRight = [&] { auto result = 0; for (const auto &child : block.children) { result = std::max(result, BlockContentMaxRight(child, st)); } return result; }; if (block.insideHorizontalScroll || (!block.scrollViewportRect.isEmpty() && block.horizontalScrollMax > 0)) { return outerRight; } switch (block.kind) { case PreparedBlockKind::Paragraph: case PreparedBlockKind::Thinking: case PreparedBlockKind::Heading: { if (block.flowTextAlign != style::al_left) { return outerRight; } const auto &leaf = block.placeholderLeaf.isEmpty() ? block.leaf : block.placeholderLeaf; if (leaf.isEmpty() || block.textRect.isEmpty()) { return 0; } const auto width = std::min( std::max( leaf.maxWidth(), ReadableTextMinWidth(LaidOutFlowTextStyle(block, st))), block.textRect.width()); return std::min(block.textRect.x() + width, outerRight); } case PreparedBlockKind::CodeBlock: return std::min( std::max( textRight() + BlockquotePadding(st.code.pre).right(), block.outer.x() + CodeBlockMinimumWidth(st)), outerRight); case PreparedBlockKind::Rule: return block.outer.x(); case PreparedBlockKind::DisplayMath: { if (!block.textRect.isEmpty()) { return outerRight; } const auto &padding = st.displayMath.padding; const auto formulaWidth = block.formulaRect.isEmpty() ? 1 : block.formulaRect.width(); return std::min( block.outer.x() + padding.left() + formulaWidth + padding.right(), outerRight); } case PreparedBlockKind::Table: { const auto tableRight = block.tableRect.isEmpty() ? 0 : (block.tableRect.x() + block.tableRect.width()); return std::min(std::max(textRight(), tableRight), outerRight); } case PreparedBlockKind::List: return std::min(childrenRight(), outerRight); case PreparedBlockKind::ListItem: return std::min( std::max( block.outer.x() + block.markerWidth, childrenRight()), outerRight); case PreparedBlockKind::Quote: return block.pullquote ? outerRight : std::min( childrenRight() + BlockquotePadding(st.body.blockquote).right(), outerRight); case PreparedBlockKind::Photo: case PreparedBlockKind::Video: case PreparedBlockKind::Audio: case PreparedBlockKind::Map: case PreparedBlockKind::Channel: case PreparedBlockKind::GroupedMedia: case PreparedBlockKind::RelatedArticle: case PreparedBlockKind::Placeholder: case PreparedBlockKind::Details: case PreparedBlockKind::EmbedPost: return outerRight; } return outerRight; } [[nodiscard]] std::optional RecountBlocksInPlace( const std::vector &prepared, const std::vector &formulas, std::vector *blocks, const std::vector &analysis, const WidthAnalysisNode *activeScrollOwner, const style::Markdown &st, int left, int top, int width, int logicalWidth, LayoutContext context) { if (!blocks || prepared.size() != blocks->size() || prepared.size() != analysis.size()) { return std::nullopt; } auto y = top; auto previous = static_cast(nullptr); for (auto i = 0, count = int(prepared.size()); i != count; ++i) { const auto &preparedBlock = prepared[i]; auto &live = (*blocks)[i]; const auto anchorOnly = IsAnchorOnlyBlock(preparedBlock); const auto next = NextVisibleBlock(prepared, i); auto blockContext = context; blockContext.preparedPath.push_back(i); if (HideEmptyQuoteAuthorBlock(preparedBlock, blockContext)) { live = HiddenQuoteAuthorBlock(preparedBlock); continue; } if (previous && !anchorOnly) { y += BlockSkip(*previous, preparedBlock, context, st); } const auto band = BlockBand( preparedBlock.kind, st, left, std::max(width, 1), context); const auto logicalBandWidth = BlockBandWidth( preparedBlock.kind, st, logicalWidth, context); const auto bottom = IsRelatedArticlesHeader(preparedBlock, next) ? RecountSimpleLaidOutBlock( preparedBlock, formulas, &live, st, left + st.relatedArticle.headerPadding.left(), y + st.relatedArticle.headerPadding.top(), std::max( width - st.relatedArticle.headerPadding.left() - st.relatedArticle.headerPadding.right(), 1), std::max( logicalWidth - st.relatedArticle.headerPadding.left() - st.relatedArticle.headerPadding.right(), 1), false, blockContext) : RecountBlockInPlace( preparedBlock, formulas, &live, analysis[i], activeScrollOwner, st, band.x(), y, band.width(), logicalBandWidth, blockContext); if (!bottom) { return std::nullopt; } if (IsRelatedArticlesHeader(preparedBlock, next)) { live.headerRect = QRect( left, y, std::max(width, 1), live.outer.height() + st.relatedArticle.headerPadding.top() + st.relatedArticle.headerPadding.bottom()); live.outer = live.headerRect; live.contentRect = live.headerRect; RefreshLogicalGeometry(&live); } y = BlockBottom(live); if (!anchorOnly) { previous = &preparedBlock; } } return y; } } // namespace int LayoutBlocks( const std::vector &prepared, std::vector *formulas, std::vector *renderedFormulas, MathRenderer *renderer, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, std::vector *blocks, const style::Markdown &st, int left, int top, int width, LayoutContext context) { const auto analysis = AnalyzeBlocks( prepared, *formulas, inlineFormulaObjects, mediaRuntime, st, width, context); return LayoutBlocks( prepared, formulas, renderedFormulas, renderer, inlineFormulaObjects, mediaRuntime, blocks, analysis, nullptr, st, left, top, width, width, context); } std::optional RecountLaidOutBlocks( const std::vector &prepared, const std::vector &formulas, std::vector *blocks, const style::Markdown &st, int left, int top, int width, LayoutContext context) { if (!blocks) { return std::nullopt; } auto analysis = AnalyzeRetainedBlocks( prepared, *blocks, formulas, st, width, context); if (!analysis) { return std::nullopt; } return RecountBlocksInPlace( prepared, formulas, blocks, *analysis, nullptr, st, left, top, width, width, context); } int ArticleContentMaxRight( const std::vector &blocks, const style::Markdown &st) { auto result = 0; for (const auto &block : blocks) { const auto bandPadding = UsesMediaBand(block.kind) ? st.mediaPadding : st.textPadding; result = std::max( result, BlockContentMaxRight(block, st) - bandPadding.left()); } return result; } } // namespace Iv::Markdown