mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-07 20:26:25 +00:00
1881 lines
50 KiB
C++
1881 lines
50 KiB
C++
/*
|
|
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 <algorithm>
|
|
|
|
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);
|
|
}
|
|
|
|
void SetEditPlaceholderLeaf(
|
|
LaidOutBlock *block,
|
|
const QString &text,
|
|
const style::TextStyle &textStyle,
|
|
int width) {
|
|
if (text.isEmpty()) {
|
|
return;
|
|
}
|
|
block->placeholderText = text;
|
|
block->placeholderLeaf = Ui::Text::String(TextMinResizeWidth(width));
|
|
block->placeholderLeaf.setMarkedText(
|
|
textStyle,
|
|
TextWithEntities::Simple(block->placeholderText),
|
|
TextParseOptions{
|
|
TextParseMultiline,
|
|
0,
|
|
0,
|
|
Qt::LayoutDirectionAuto,
|
|
});
|
|
}
|
|
|
|
[[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]] 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<PreparedBlock> &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);
|
|
}
|
|
|
|
[[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,
|
|
};
|
|
}
|
|
|
|
struct WidthAnalysisNode {
|
|
std::vector<WidthAnalysisNode> children;
|
|
int contentMinimumWidth = 1;
|
|
int outerMinimumWidth = 1;
|
|
int scrollOwnerMinimumWidth = 1;
|
|
bool ownerEligible = false;
|
|
bool chosenScrollOwner = false;
|
|
bool subtreeHasScrollOwner = false;
|
|
};
|
|
|
|
[[nodiscard]] int MaxChildOuterMinimumWidth(
|
|
const std::vector<WidthAnalysisNode> &children) {
|
|
auto result = 1;
|
|
for (const auto &child : children) {
|
|
result = std::max(result, child.outerMinimumWidth);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void FinalizeOwnerSelection(
|
|
WidthAnalysisNode *analysis,
|
|
int availableWidth) {
|
|
analysis->subtreeHasScrollOwner = false;
|
|
for (const auto &child : analysis->children) {
|
|
analysis->subtreeHasScrollOwner |= child.subtreeHasScrollOwner;
|
|
}
|
|
analysis->chosenScrollOwner = !analysis->subtreeHasScrollOwner
|
|
&& analysis->ownerEligible
|
|
&& (analysis->scrollOwnerMinimumWidth > std::max(availableWidth, 1));
|
|
analysis->subtreeHasScrollOwner |= analysis->chosenScrollOwner;
|
|
}
|
|
|
|
[[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 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<PreparedFormulaSlot> &formulas,
|
|
const style::Markdown &st,
|
|
int width,
|
|
LayoutContext context);
|
|
|
|
[[nodiscard]] std::vector<WidthAnalysisNode> AnalyzeBlocks(
|
|
const std::vector<PreparedBlock> &prepared,
|
|
const std::vector<PreparedFormulaSlot> &formulas,
|
|
const style::Markdown &st,
|
|
int width,
|
|
LayoutContext context) {
|
|
auto result = std::vector<WidthAnalysisNode>();
|
|
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 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,
|
|
st,
|
|
blockWidth,
|
|
context));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] WidthAnalysisNode AnalyzeBlock(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> &formulas,
|
|
const style::Markdown &st,
|
|
int width,
|
|
LayoutContext context) {
|
|
auto analysis = WidthAnalysisNode();
|
|
const auto availableWidth = std::max(width, 1);
|
|
switch (prepared.kind) {
|
|
case PreparedBlockKind::Paragraph:
|
|
case PreparedBlockKind::Thinking:
|
|
case PreparedBlockKind::Heading:
|
|
analysis.contentMinimumWidth = FlowBlockMinimumWidth(prepared, st);
|
|
analysis.outerMinimumWidth = analysis.contentMinimumWidth;
|
|
analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth;
|
|
analysis.ownerEligible = !IsAnchorOnlyBlock(prepared);
|
|
break;
|
|
case PreparedBlockKind::CodeBlock:
|
|
analysis.contentMinimumWidth = CodeBlockMinimumWidth(st);
|
|
analysis.outerMinimumWidth = analysis.contentMinimumWidth;
|
|
analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth;
|
|
analysis.ownerEligible = true;
|
|
break;
|
|
case PreparedBlockKind::DisplayMath:
|
|
analysis.contentMinimumWidth = DisplayMathMinimumWidth(
|
|
prepared,
|
|
formulas,
|
|
st);
|
|
analysis.outerMinimumWidth = analysis.contentMinimumWidth;
|
|
analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth;
|
|
analysis.ownerEligible = true;
|
|
break;
|
|
case PreparedBlockKind::Table:
|
|
analysis.contentMinimumWidth = TableBlockMinimumWidth(prepared, st);
|
|
analysis.outerMinimumWidth = analysis.contentMinimumWidth;
|
|
analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth;
|
|
analysis.ownerEligible = true;
|
|
break;
|
|
case PreparedBlockKind::List: {
|
|
const auto depthDelta = std::max(
|
|
prepared.visualDepth - context.listDepth,
|
|
0);
|
|
const auto overhead = depthDelta * st.list.indent;
|
|
const auto childWidth = std::max(availableWidth - overhead, 1);
|
|
auto childContext = context;
|
|
childContext.listDepth = prepared.visualDepth;
|
|
childContext.tightList = false;
|
|
PrepareNestedContext(&childContext, 0, childWidth);
|
|
analysis.children = AnalyzeBlocks(
|
|
prepared.children,
|
|
formulas,
|
|
st,
|
|
childWidth,
|
|
childContext);
|
|
analysis.contentMinimumWidth = MaxChildOuterMinimumWidth(analysis.children);
|
|
analysis.outerMinimumWidth = overhead + analysis.contentMinimumWidth;
|
|
analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth;
|
|
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);
|
|
auto childContext = context;
|
|
childContext.tightList = false;
|
|
PrepareNestedContext(&childContext, 0, childWidth);
|
|
analysis.children = AnalyzeBlocks(
|
|
prepared.children,
|
|
formulas,
|
|
st,
|
|
childWidth,
|
|
childContext);
|
|
analysis.contentMinimumWidth = MaxChildOuterMinimumWidth(analysis.children);
|
|
analysis.outerMinimumWidth = overhead + analysis.contentMinimumWidth;
|
|
analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth;
|
|
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);
|
|
auto childWidth = std::max(
|
|
availableWidth - overhead - HorizontalMarginsWidth(padding),
|
|
1);
|
|
if (prepared.pullquote) {
|
|
childWidth = std::min(childWidth, std::max(st.pullquote.maxWidth, 1));
|
|
}
|
|
auto childContext = context;
|
|
childContext.quoteDepth = prepared.visualDepth;
|
|
childContext.tightList = false;
|
|
PrepareNestedContext(&childContext, 0, childWidth);
|
|
analysis.children = AnalyzeBlocks(
|
|
prepared.children,
|
|
formulas,
|
|
st,
|
|
childWidth,
|
|
childContext);
|
|
analysis.contentMinimumWidth = MaxChildOuterMinimumWidth(analysis.children);
|
|
const auto visibleContentWidth = prepared.pullquote
|
|
? std::min(
|
|
analysis.contentMinimumWidth,
|
|
std::max(st.pullquote.maxWidth, 1))
|
|
: analysis.contentMinimumWidth;
|
|
analysis.outerMinimumWidth = overhead
|
|
+ HorizontalMarginsWidth(padding)
|
|
+ visibleContentWidth;
|
|
analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth;
|
|
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
|
|
+ ReadableTextMinWidth(st.details.summaryStyle);
|
|
auto bodyMinimumWidth = 1;
|
|
if (!prepared.collapsed) {
|
|
const auto childWidth = std::max(
|
|
availableWidth - HorizontalMarginsWidth(bodyPadding),
|
|
1);
|
|
auto childContext = context;
|
|
PrepareNestedContext(&childContext, 0, childWidth);
|
|
analysis.children = AnalyzeBlocks(
|
|
prepared.children,
|
|
formulas,
|
|
st,
|
|
childWidth,
|
|
childContext);
|
|
analysis.contentMinimumWidth = MaxChildOuterMinimumWidth(
|
|
analysis.children);
|
|
bodyMinimumWidth = ContainerMinimumWidth(
|
|
analysis.contentMinimumWidth,
|
|
bodyPadding);
|
|
}
|
|
analysis.outerMinimumWidth = std::max(
|
|
headerMinimumWidth,
|
|
bodyMinimumWidth);
|
|
analysis.scrollOwnerMinimumWidth = 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;
|
|
if (!prepared.embedPost.author.isEmpty()) {
|
|
headerTextWidth = std::max(
|
|
headerTextWidth,
|
|
ReadableTextMinWidth(style.authorStyle));
|
|
}
|
|
if (!prepared.embedPost.dateText.isEmpty()) {
|
|
headerTextWidth = std::max(
|
|
headerTextWidth,
|
|
ReadableTextMinWidth(style.dateStyle));
|
|
}
|
|
const auto childWidth = std::max(availableWidth - contentOverhead, 1);
|
|
auto childContext = context;
|
|
PrepareNestedContext(&childContext, 0, childWidth);
|
|
analysis.children = AnalyzeBlocks(
|
|
prepared.children,
|
|
formulas,
|
|
st,
|
|
childWidth,
|
|
childContext);
|
|
analysis.contentMinimumWidth = MaxChildOuterMinimumWidth(analysis.children);
|
|
analysis.scrollOwnerMinimumWidth = contentOverhead
|
|
+ analysis.contentMinimumWidth;
|
|
analysis.outerMinimumWidth = std::max(
|
|
contentOverhead + avatarWidth + headerGap + headerTextWidth,
|
|
analysis.scrollOwnerMinimumWidth);
|
|
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.outerMinimumWidth = 1;
|
|
analysis.scrollOwnerMinimumWidth = analysis.outerMinimumWidth;
|
|
analysis.ownerEligible = false;
|
|
break;
|
|
}
|
|
FinalizeOwnerSelection(&analysis, availableWidth);
|
|
return analysis;
|
|
}
|
|
|
|
[[nodiscard]] LaidOutBlock LayoutBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &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<PreparedBlock> &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
std::vector<LaidOutBlock> *blocks,
|
|
const std::vector<WidthAnalysisNode> &analysis,
|
|
const WidthAnalysisNode *activeScrollOwner,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
LayoutContext context);
|
|
|
|
[[nodiscard]] LaidOutBlock LayoutListItemBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &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 &list = st.list;
|
|
const auto bodyLineHeight = TextLineHeight(st.body);
|
|
const auto task = (prepared.taskState != TaskState::None);
|
|
const auto ordered = !task && (prepared.listKind == ListKind::Ordered);
|
|
const auto markerText = ordered ? ListMarkerText(prepared) : QString();
|
|
auto markerTextWidth = 0;
|
|
auto markerTextHeight = bodyLineHeight;
|
|
if (task
|
|
&& prepared.editListItem
|
|
&& context.taskMarkerRippleRuntimeFactory) {
|
|
block.taskMarkerRippleRuntime
|
|
= context.taskMarkerRippleRuntimeFactory(*prepared.editListItem);
|
|
}
|
|
if (task) {
|
|
markerTextWidth = list.taskCheck.diameter;
|
|
markerTextHeight = list.taskCheck.diameter;
|
|
} else if (ordered) {
|
|
block.marker.setMarkedText(
|
|
st.body,
|
|
TextWithEntities::Simple(markerText),
|
|
TextParseOptions{
|
|
TextParseMultiline,
|
|
0,
|
|
0,
|
|
Qt::LayoutDirectionAuto,
|
|
});
|
|
markerTextWidth = std::max(block.marker.maxWidth(), 1);
|
|
markerTextHeight = std::max(
|
|
block.marker.countHeight(markerTextWidth, true),
|
|
bodyLineHeight);
|
|
}
|
|
|
|
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;
|
|
|
|
auto childContext = context;
|
|
childContext.tightList = tight;
|
|
PrepareNestedContext(&childContext, bodyLeft, bodyWidth);
|
|
const auto childBottom = LayoutBlocks(
|
|
prepared.children,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
&block.children,
|
|
analysis.children,
|
|
childActiveScrollOwner,
|
|
st,
|
|
bodyLeft,
|
|
top,
|
|
bodyWidth,
|
|
bodyLogicalWidth,
|
|
childContext);
|
|
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);
|
|
}
|
|
}
|
|
const auto scrollbarHeight = ScrollbarReserveHeight(
|
|
scrollOwner,
|
|
block.horizontalScrollMax,
|
|
st);
|
|
block.outer = QRect(
|
|
left,
|
|
top,
|
|
std::max(width, 1),
|
|
rowHeight + scrollbarHeight);
|
|
block.firstLineBaseline = markerBaseline;
|
|
RefreshLogicalGeometry(&block);
|
|
return block;
|
|
}
|
|
|
|
[[nodiscard]] LaidOutBlock LayoutListBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &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 depthDelta = std::max(prepared.visualDepth - context.listDepth, 0);
|
|
const auto listLeft = left + depthDelta * st.list.indent;
|
|
const auto listWidth = std::max(
|
|
width - depthDelta * st.list.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 - depthDelta * st.list.indent,
|
|
1);
|
|
const auto childActiveScrollOwner = currentScrollOwner;
|
|
|
|
auto childContext = context;
|
|
childContext.listDepth = prepared.visualDepth;
|
|
childContext.tightList = false;
|
|
PrepareNestedContext(&childContext, listLeft, listWidth);
|
|
|
|
auto y = top;
|
|
auto previous = static_cast<const PreparedBlock*>(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);
|
|
}
|
|
|
|
auto laidOut = (child.kind == PreparedBlockKind::ListItem)
|
|
? LayoutListItemBlock(
|
|
child,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
analysis.children[i],
|
|
childActiveScrollOwner,
|
|
st,
|
|
listLeft,
|
|
y,
|
|
listWidth,
|
|
listLogicalWidth,
|
|
childContext,
|
|
prepared.tight)
|
|
: LayoutBlock(
|
|
child,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
analysis.children[i],
|
|
childActiveScrollOwner,
|
|
st,
|
|
listLeft,
|
|
y,
|
|
listWidth,
|
|
listLogicalWidth,
|
|
childContext);
|
|
y = BlockBottom(laidOut);
|
|
block.children.push_back(std::move(laidOut));
|
|
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 block;
|
|
}
|
|
|
|
[[nodiscard]] LaidOutBlock LayoutQuoteBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &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 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 pullquote = prepared.pullquote;
|
|
const auto padding = pullquote
|
|
? st.pullquote.padding
|
|
: BlockquotePadding(st.body.blockquote);
|
|
const auto availableWidth = std::max(
|
|
quoteWidth - padding.left() - padding.right(),
|
|
1);
|
|
const auto contentWidth = pullquote
|
|
? std::min(availableWidth, std::max(st.pullquote.maxWidth, 1))
|
|
: availableWidth;
|
|
const auto logicalAvailableWidth = std::max(
|
|
quoteLogicalWidth - padding.left() - padding.right(),
|
|
1);
|
|
const auto contentLogicalWidth = pullquote
|
|
? std::min(logicalAvailableWidth, std::max(st.pullquote.maxWidth, 1))
|
|
: logicalAvailableWidth;
|
|
const auto contentLeft = pullquote
|
|
? (quoteLeft + padding.left() + ((availableWidth - contentWidth) / 2))
|
|
: (quoteLeft + padding.left());
|
|
const auto contentTop = top + padding.top();
|
|
const auto childActiveScrollOwner = NextActiveScrollOwner(
|
|
analysis,
|
|
activeScrollOwner);
|
|
|
|
auto childContext = context;
|
|
childContext.quoteDepth = prepared.visualDepth;
|
|
childContext.tightList = false;
|
|
PrepareNestedContext(&childContext, contentLeft, contentWidth);
|
|
const auto childBottom = LayoutBlocks(
|
|
prepared.children,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
&block.children,
|
|
analysis.children,
|
|
childActiveScrollOwner,
|
|
st,
|
|
contentLeft,
|
|
contentTop,
|
|
contentWidth,
|
|
contentLogicalWidth,
|
|
childContext);
|
|
const auto contentHeight = std::max(
|
|
childBottom - contentTop,
|
|
prepared.children.empty()
|
|
? TextLineHeight(st.body)
|
|
: 0);
|
|
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);
|
|
|
|
block.outer = QRect(quoteLeft, top, quoteWidth, quoteHeight);
|
|
block.contentRect = QRect(
|
|
contentLeft,
|
|
contentTop,
|
|
contentWidth,
|
|
contentHeight);
|
|
block.firstLineBaseline = ResolveFirstDisplayedLineBaseline(block, st);
|
|
RefreshLogicalGeometry(&block);
|
|
return block;
|
|
}
|
|
|
|
[[nodiscard]] LaidOutBlock LayoutDetailsBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &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;
|
|
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 iconSize = details.icon.empty()
|
|
? 0
|
|
: TextLineHeight(details.summaryStyle);
|
|
const auto iconWidth = iconSize;
|
|
const auto iconHeight = iconSize;
|
|
const auto iconSkip = iconWidth ? details.iconSkip : 0;
|
|
const auto actionWidth = context.editMode
|
|
? DetailsStateReserveWidth(st)
|
|
: 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);
|
|
|
|
SetTextLeaf(
|
|
&block.leaf,
|
|
details.summaryStyle,
|
|
st,
|
|
prepared.text,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
block.textWidth);
|
|
BindLinks(&block.leaf, prepared.links);
|
|
const auto usePlaceholder = prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty();
|
|
if (usePlaceholder) {
|
|
SetEditPlaceholderLeaf(
|
|
&block,
|
|
prepared.editPlaceholderText,
|
|
details.summaryStyle,
|
|
block.textWidth);
|
|
}
|
|
const auto &displayLeaf = usePlaceholder
|
|
? block.placeholderLeaf
|
|
: block.leaf;
|
|
|
|
const auto summaryHeight = ResolveTextLeafHeight(
|
|
std::max(
|
|
displayLeaf.countHeight(block.textWidth, true),
|
|
TextLineHeight(details.summaryStyle)),
|
|
context);
|
|
auto actionHeight = 0;
|
|
if (actionWidth > 0) {
|
|
block.actionWidth = actionWidth;
|
|
SetTextLeaf(
|
|
&block.actionLeaf,
|
|
details.summaryStyle,
|
|
st,
|
|
TextWithEntities::Simple(
|
|
DetailsStateText(prepared.detailsOpen)),
|
|
nullptr,
|
|
nullptr,
|
|
mediaRuntime,
|
|
block.actionWidth);
|
|
actionHeight = std::max(
|
|
block.actionLeaf.countHeight(block.actionWidth, true),
|
|
TextLineHeight(details.summaryStyle));
|
|
}
|
|
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);
|
|
auto childContext = context;
|
|
PrepareNestedContext(&childContext, childLeft, childWidth);
|
|
const auto childBottom = LayoutBlocks(
|
|
prepared.children,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
&block.children,
|
|
analysis.children,
|
|
childActiveScrollOwner,
|
|
st,
|
|
childLeft,
|
|
childTop,
|
|
childWidth,
|
|
childLogicalWidth,
|
|
childContext);
|
|
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 block;
|
|
}
|
|
|
|
[[nodiscard]] LaidOutBlock LayoutEmbedPostBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &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 parseOptions = TextParseOptions{
|
|
TextParseMultiline,
|
|
0,
|
|
0,
|
|
Qt::LayoutDirectionAuto,
|
|
};
|
|
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;
|
|
block.labelLeaf = Ui::Text::String(TextMinResizeWidth(textWidth));
|
|
block.labelLeaf.setMarkedText(
|
|
style.authorStyle,
|
|
TextWithEntities::Simple(prepared.embedPost.author),
|
|
parseOptions);
|
|
authorHeight = ResolveTextLeafHeight(
|
|
std::max(
|
|
block.labelLeaf.countHeight(textWidth, true),
|
|
TextLineHeight(style.authorStyle)),
|
|
context);
|
|
}
|
|
|
|
auto dateHeight = 0;
|
|
if (!prepared.embedPost.dateText.isEmpty()) {
|
|
block.subtitleWidth = textWidth;
|
|
block.subtitleLeaf = Ui::Text::String(TextMinResizeWidth(textWidth));
|
|
block.subtitleLeaf.setMarkedText(
|
|
style.dateStyle,
|
|
TextWithEntities::Simple(prepared.embedPost.dateText),
|
|
parseOptions);
|
|
dateHeight = ResolveTextLeafHeight(
|
|
std::max(
|
|
block.subtitleLeaf.countHeight(textWidth, true),
|
|
TextLineHeight(style.dateStyle)),
|
|
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);
|
|
auto childContext = context;
|
|
PrepareNestedContext(&childContext, contentLeft, contentWidth);
|
|
const auto childBottom = LayoutBlocks(
|
|
prepared.children,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
&block.children,
|
|
analysis.children,
|
|
childActiveScrollOwner,
|
|
st,
|
|
contentLeft,
|
|
bodyTop,
|
|
contentWidth,
|
|
contentLogicalWidth,
|
|
childContext);
|
|
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();
|
|
LayoutMediaCaption(
|
|
&block,
|
|
prepared,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
left,
|
|
bottom,
|
|
blockWidth,
|
|
style.captionSkip,
|
|
&bottom,
|
|
context);
|
|
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 block;
|
|
}
|
|
|
|
[[nodiscard]] LaidOutBlock LayoutBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &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);
|
|
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);
|
|
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<PreparedBlock> &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
std::vector<LaidOutBlock> *blocks,
|
|
const std::vector<WidthAnalysisNode> &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<const PreparedBlock*>(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);
|
|
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,
|
|
context)
|
|
: LayoutBlock(
|
|
block,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
analysis[i],
|
|
activeScrollOwner,
|
|
st,
|
|
band.x(),
|
|
y,
|
|
band.width(),
|
|
logicalBandWidth,
|
|
context);
|
|
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;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int LayoutBlocks(
|
|
const std::vector<PreparedBlock> &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
std::vector<RenderedFormula> *renderedFormulas,
|
|
MathRenderer *renderer,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
std::vector<LaidOutBlock> *blocks,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
LayoutContext context) {
|
|
const auto analysis = AnalyzeBlocks(
|
|
prepared,
|
|
*formulas,
|
|
st,
|
|
width,
|
|
context);
|
|
return LayoutBlocks(
|
|
prepared,
|
|
formulas,
|
|
renderedFormulas,
|
|
renderer,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
blocks,
|
|
analysis,
|
|
nullptr,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
width,
|
|
context);
|
|
}
|
|
|
|
} // namespace Iv::Markdown
|