/* 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_blocks.h" #include "iv/markdown/iv_markdown_article_text.h" #include "lang/lang_keys.h" #include "spellcheck/spellcheck_highlight_syntax.h" #include "ui/grouped_layout.h" #include "styles/style_iv.h" #include "styles/style_widgets.h" #include #include #include namespace Iv::Markdown { namespace { constexpr auto kIvMarkedTextOptions = TextParseOptions{ TextParseMultiline, 0, 0, Qt::LayoutDirectionAuto, }; constexpr auto kCodeTabColumns = 4; constexpr auto kCodeTrailingGuard = 0x2060; const auto kPhotoCopyLabel = u"Photo"_q; const auto kUsernamePrefix = u"@"_q; [[nodiscard]] style::align CellAlign(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]] const style::TextStyle &TableCellTextStyle( bool header, const style::Markdown &) { if (header) { return st::defaultTable.defaultLabel.style; } return st::defaultTable.defaultValue.style; } [[nodiscard]] TableCellLayoutData InitializeTableCellLayout( const PreparedTableCell &prepared, bool header, const std::vector *formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &markdown) { auto result = TableCellLayoutData(); const auto &textStyle = TableCellTextStyle(header, markdown); result.cell.align = CellAlign(prepared.alignment); SetTextLeaf( &result.cell.leaf, textStyle, prepared.text, formulas, inlineFormulaObjects, mediaRuntime, TableCellTextMinResizeWidth(textStyle, markdown)); BindLinks(&result.cell.leaf, prepared.links); result.preferredWidth = result.cell.leaf.maxWidth(); result.preferredHeight = std::max( result.cell.leaf.countHeight(std::max(result.preferredWidth, 1), true), TextLineHeight(textStyle)); return result; } [[nodiscard]] std::vector ComputeTableColumnWidths( const std::vector &rows, int columnCount, int width, const style::Markdown &markdown, bool *overflowed) { const auto &padding = markdown.table.cellPadding; const auto border = st::defaultTable.border; const auto minimum = markdown.table.minColumnWidth; auto result = std::vector(std::max(columnCount, 0), minimum); auto preferred = std::vector(std::max(columnCount, 0), minimum); for (const auto &row : rows) { for (auto column = 0; column != columnCount; ++column) { preferred[column] = std::max( preferred[column], row.cells[column].preferredWidth + padding.left() + padding.right()); } } const auto availableWidth = std::max(width, 1); const auto minimumGridWidth = border + columnCount * (minimum + border); *overflowed = (minimumGridWidth > availableWidth); if (*overflowed || !columnCount) { return result; } auto extra = availableWidth - minimumGridWidth; auto remaining = 0; auto deficits = std::vector(columnCount, 0); for (auto column = 0; column != columnCount; ++column) { deficits[column] = std::max(preferred[column] - minimum, 0); remaining += deficits[column]; } while (extra > 0 && remaining > 0) { auto active = 0; for (const auto deficit : deficits) { if (deficit > 0) { ++active; } } if (!active) { break; } const auto step = std::max(extra / active, 1); for (auto column = 0; column != columnCount && extra > 0; ++column) { if (!deficits[column]) { continue; } const auto delta = std::min({ deficits[column], step, extra }); result[column] += delta; deficits[column] -= delta; remaining -= delta; extra -= delta; } } if (extra > 0) { const auto base = extra / columnCount; const auto tail = extra % columnCount; for (auto column = 0; column != columnCount; ++column) { result[column] += base + ((column < tail) ? 1 : 0); } } return result; } template void ResolveRuntimeImages( const std::shared_ptr &runtime, QSize size, std::shared_ptr *thumbnail, std::shared_ptr *full) { if (!runtime) { return; } if (thumbnail) { *thumbnail = runtime->thumbnail(size); } if (full) { *full = runtime->full(size); } } void SetPlainTextLeaf( Ui::Text::String *leaf, const style::TextStyle &textStyle, const QString &text, int width) { *leaf = Ui::Text::String(TextMinResizeWidth(width)); leaf->setMarkedText( textStyle, TextWithEntities::Simple(text), kIvMarkedTextOptions); } [[nodiscard]] int LeafHeight( const Ui::Text::String &leaf, const style::TextStyle &textStyle, int width) { return std::max( leaf.countHeight(width, true), TextLineHeight(textStyle)); } [[nodiscard]] int MediaHeightForWidth( int width, int aspectWidth, int aspectHeight) { aspectWidth = std::max(aspectWidth, 1); aspectHeight = std::max(aspectHeight, 1); return std::max( int((int64(width) * aspectHeight + aspectWidth - 1) / aspectWidth), 1); } [[nodiscard]] int GroupedMediaMinWidth(int width, int spacing) { return std::max((width - 2 * spacing) / 3, 1); } void LayoutMediaCaption( LaidOutBlock *block, const PreparedBlock &prepared, const std::vector *formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &markdown, int left, int top, int width, int skip, int *bottom) { if (prepared.text.text.isEmpty()) { return; } block->textWidth = std::max(width, 1); SetTextLeaf( &block->leaf, markdown.body, prepared.text, formulas, inlineFormulaObjects, mediaRuntime, block->textWidth); BindLinks(&block->leaf, prepared.links); const auto captionTop = top + skip; const auto captionHeight = std::max( block->leaf.countHeight(block->textWidth, true), TextLineHeight(markdown.body)); block->textRect = QRect(left, captionTop, block->textWidth, captionHeight); *bottom = captionTop + captionHeight; } [[nodiscard]] QString AudioTitleText(const PreparedAudioBlockData &audio) { if (!audio.title.isEmpty()) { return audio.title; } if (!audio.fileName.isEmpty()) { return audio.fileName; } return tr::lng_in_dlg_audio_file(tr::now); } [[nodiscard]] QString AudioSubtitleText(const PreparedAudioBlockData &audio) { if (!audio.performer.isEmpty()) { return audio.performer; } if (!audio.fileName.isEmpty() && audio.fileName != AudioTitleText(audio)) { return audio.fileName; } return QString(); } [[nodiscard]] QString AudioCopyText(const PreparedAudioBlockData &audio) { const auto title = AudioTitleText(audio); const auto subtitle = AudioSubtitleText(audio); return subtitle.isEmpty() ? title : (title + u"\n"_q + subtitle); } [[nodiscard]] QString ChannelSubtitleText( const PreparedChannelBlockData &channel) { return channel.username.isEmpty() ? QString() : (kUsernamePrefix + channel.username); } [[nodiscard]] QString ChannelCopyText(const PreparedChannelBlockData &channel) { const auto subtitle = ChannelSubtitleText(channel); return subtitle.isEmpty() ? channel.title : (channel.title + u"\n"_q + subtitle); } [[nodiscard]] QString GroupedMediaCopyText( const PreparedGroupedMediaBlockData &grouped) { auto photos = 0; auto videos = 0; for (const auto &item : grouped.items) { if (item.media.kind == PreparedMediaItemKind::Photo) { ++photos; } else { ++videos; } } if (photos && !videos) { return tr::lng_media_selected_photo(tr::now, lt_count, photos); } else if (videos && !photos) { return tr::lng_media_selected_video(tr::now, lt_count, videos); } return QString(); } [[nodiscard]] QString GroupedMediaItemCopyText(PreparedMediaItemKind kind) { return (kind == PreparedMediaItemKind::Photo) ? kPhotoCopyLabel : tr::lng_in_dlg_video(tr::now); } [[nodiscard]] int GroupedMediaLayoutWidth( const std::vector &layout) { auto result = 0; for (const auto &part : layout) { result = std::max( result, part.geometry.x() + part.geometry.width()); } return result; } [[nodiscard]] int GroupedMediaLayoutHeight( const std::vector &layout) { auto result = 0; for (const auto &part : layout) { result = std::max( result, part.geometry.y() + part.geometry.height()); } return result; } void ResolveGroupedMediaItemLayout( LaidOutGroupedMediaItem *item, const PreparedGroupedMediaItemData &prepared, const std::shared_ptr &mediaRuntime, QRect rect) { if (!item) { return; } item->kind = prepared.media.kind; item->copyText = GroupedMediaItemCopyText(prepared.media.kind); item->rect = rect; if (prepared.media.kind == PreparedMediaItemKind::Photo) { if (mediaRuntime) { item->photoRuntime = mediaRuntime->resolvePhoto(prepared.media.id); } ResolveRuntimeImages( item->photoRuntime, rect.size(), &item->thumbnailImage, &item->fullImage); if (item->photoRuntime) { item->activation.kind = MediaActivationKind::Photo; item->activation.photo = item->photoRuntime; } return; } if (mediaRuntime) { item->documentRuntime = mediaRuntime->resolveDocument(prepared.media.id); } ResolveRuntimeImages( item->documentRuntime, rect.size(), &item->thumbnailImage, &item->fullImage); if (item->documentRuntime) { item->activation.kind = MediaActivationKind::Document; item->activation.document = item->documentRuntime; } } } // namespace [[nodiscard]] int SingleDigitOrderedMarkerWidth( const style::Markdown &markdown) { return std::max( markdown.body.font->width(u"8."_q), markdown.body.font->width(u"8)"_q)); } QString CodeBlockDisplayText(const QString &text) { auto result = QString(); result.reserve(text.size()); auto column = 0; for (const auto ch : text) { if (ch == QChar::Tabulation) { const auto count = kCodeTabColumns - (column % kCodeTabColumns); for (auto i = 0; i != count; ++i) { result.append(QChar::Space); } column += count; continue; } result.append(ch); if (Ui::Text::IsNewline(ch)) { column = 0; } else { ++column; } } if (result.isEmpty() || Ui::Text::IsTrimmed(result.back())) { result.append(QChar(kCodeTrailingGuard)); } return result; } bool IsFlowKind(PreparedBlockKind kind) { return (kind == PreparedBlockKind::Paragraph) || (kind == PreparedBlockKind::Heading); } QString ListMarkerText(const PreparedBlock &block) { if (block.listKind == ListKind::Ordered) { const auto delimiter = (block.listDelimiter == ListDelimiter::Parenthesis) ? u")"_q : u"."_q; return QString::number(block.orderedNumber) + delimiter; } return QString(); } int TextLineHeight(const style::TextStyle &style) { return std::max(style.lineHeight, style.font->height); } [[nodiscard]] int NominalTextBaseline( const style::TextStyle &style, int top) { const auto lineHeight = TextLineHeight(style); const auto textTop = top + (std::max(lineHeight - style.font->height, 0) / 2); return textTop + style.font->ascent; } [[nodiscard]] int LeafFirstLineBaseline( const Ui::Text::String &leaf, const QRect &textRect, const style::TextStyle &style, bool breakEverywhere = true) { const auto lines = leaf.countLinesGeometry(textRect.width(), breakEverywhere); return textRect.y() + (lines.empty() ? NominalTextBaseline(style, 0) : lines.front().baseline); } QPoint BulletMarkerCenter( int left, int baseline, const style::Markdown &markdown) { const auto &list = markdown.list; const auto lineHeight = TextLineHeight(markdown.body); const auto markerWidth = SingleDigitOrderedMarkerWidth(markdown); const auto nominalBaseline = NominalTextBaseline(markdown.body, 0); return QPoint( left + list.markerWidth - list.bulletLeftShift - ((markerWidth + 1) / 2), baseline + (lineHeight / 2) - nominalBaseline); } QMargins BlockquotePadding(const style::QuoteStyle &style) { return style.padding + QMargins(0, style.header + style.verticalSkip, 0, style.verticalSkip); } Ui::Text::GeometryDescriptor TextGeometry(int width) { auto result = Ui::Text::SimpleGeometry(std::max(width, 1), 0, 0, false); result.breakEverywhere = true; return result; } int TextMinResizeWidth(int width) { return std::max(width, 1); } int TableCellTextMinResizeWidth( const style::TextStyle &textStyle, const style::Markdown &markdown) { const auto &padding = markdown.table.cellPadding; return std::max({ markdown.table.minColumnWidth - padding.left() - padding.right(), textStyle.font->spacew, 1, }); } int BlockSkip( const PreparedBlock &block, const style::Markdown &markdown) { const auto &skips = markdown.blockSkips; switch (block.kind) { case PreparedBlockKind::Paragraph: return skips.paragraph; case PreparedBlockKind::Heading: return skips.heading; case PreparedBlockKind::CodeBlock: return skips.code; case PreparedBlockKind::Rule: return skips.rule; case PreparedBlockKind::List: case PreparedBlockKind::ListItem: return skips.paragraph; case PreparedBlockKind::Quote: return skips.quote; case PreparedBlockKind::DisplayMath: return skips.displayMath; case PreparedBlockKind::Table: return skips.table; case PreparedBlockKind::Photo: return skips.photo; case PreparedBlockKind::Video: return skips.video; case PreparedBlockKind::Audio: return skips.audio; case PreparedBlockKind::Map: return skips.map; case PreparedBlockKind::Channel: return skips.channel; case PreparedBlockKind::Placeholder: return skips.placeholder; case PreparedBlockKind::Details: return skips.paragraph; case PreparedBlockKind::GroupedMedia: return skips.groupedMedia; } return 0; } int BlockSkip( const PreparedBlock &previous, const PreparedBlock &block, LayoutContext context, const style::Markdown &markdown) { if (context.tightList && IsFlowKind(previous.kind) && IsFlowKind(block.kind)) { return 0; } return BlockSkip(block, markdown); } const style::TextStyle &TextStyleFor( const PreparedBlock &block, const style::Markdown &markdown) { if (block.kind == PreparedBlockKind::CodeBlock) { return markdown.code; } else if (block.kind != PreparedBlockKind::Heading) { return markdown.body; } switch (std::clamp(block.headingLevel, 1, 6)) { case 1: return markdown.heading1; case 2: return markdown.heading2; case 3: return markdown.heading3; case 4: return markdown.heading4; case 5: return markdown.heading5; case 6: return markdown.heading6; } return markdown.heading6; } int BlockMaxRight(const std::vector &blocks) { auto result = 0; for (const auto &block : blocks) { result = std::max(result, block.outer.right() + 1); result = std::max(result, BlockMaxRight(block.children)); } return result; } void RepopulateCodeBlockLeaf( LaidOutBlock &block, const style::Markdown &markdown, bool allowAsyncSyntaxHighlighting, CodeBlockSyntaxHighlightTracker *syntaxHighlightTracker) { auto marked = tr::marked(CodeBlockDisplayText(block.copyText)); if (!marked.text.isEmpty()) { marked.entities.push_back(EntityInText( EntityType::Pre, 0, marked.text.size(), block.codeLanguage)); } block.syntaxHighlightProcessId = allowAsyncSyntaxHighlighting ? (syntaxHighlightTracker ? syntaxHighlightTracker->tryHighlightSyntax( marked.text, block.codeLanguage, marked) : Spellchecker::TryHighlightSyntax(marked)) : 0; block.leaf.setMarkedText( markdown.code, std::move(marked), kIvMarkedTextOptions); } LaidOutBlock LayoutFlowBlock( const PreparedBlock &prepared, const std::vector *formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &markdown, int left, int top, int width) { if (prepared.kind == PreparedBlockKind::GroupedMedia) { return LayoutGroupedMediaBlock( prepared, formulas, inlineFormulaObjects, mediaRuntime, markdown, left, top, width); } auto block = LaidOutBlock(); block.kind = prepared.kind; block.anchorId = prepared.anchorId; block.headingLevel = prepared.headingLevel; block.textWidth = std::max(width, 1); const auto &textStyle = TextStyleFor(prepared, markdown); SetTextLeaf( &block.leaf, textStyle, prepared.text, formulas, inlineFormulaObjects, mediaRuntime, block.textWidth); BindLinks(&block.leaf, prepared.links); const auto height = std::max( block.leaf.countHeight(block.textWidth, true), TextLineHeight(textStyle)); block.textRect = QRect(left, top, block.textWidth, height); block.outer = QRect(left, top, block.textWidth, height); block.firstLineBaseline = LeafFirstLineBaseline( block.leaf, block.textRect, textStyle); return block; } LaidOutBlock LayoutCodeBlock( const PreparedBlock &prepared, const style::Markdown &markdown, int left, int top, int width, bool allowAsyncSyntaxHighlighting, CodeBlockSyntaxHighlightTracker *syntaxHighlightTracker) { auto block = LaidOutBlock(); block.kind = PreparedBlockKind::CodeBlock; block.copyText = prepared.text.text; block.codeLanguage = prepared.codeLanguage; block.textWidth = std::max(width, 1); block.leaf = Ui::Text::String(TextMinResizeWidth(block.textWidth)); RepopulateCodeBlockLeaf( block, markdown, allowAsyncSyntaxHighlighting, syntaxHighlightTracker); const auto height = std::max( block.leaf.countHeight(block.textWidth, true), TextLineHeight(markdown.code)); block.textRect = QRect(left, top, block.textWidth, height); block.outer = block.textRect; block.firstLineBaseline = LeafFirstLineBaseline( block.leaf, block.textRect, markdown.code); return block; } LaidOutBlock LayoutRuleBlock( const style::Markdown &markdown, int left, int top, int width) { auto block = LaidOutBlock(); block.kind = PreparedBlockKind::Rule; block.outer = QRect( left, top, std::max(width, 1), markdown.rule.height); block.textRect = block.outer; return block; } LaidOutBlock LayoutDisplayMathBlock( const PreparedBlock &prepared, const std::vector &formulas, const style::Markdown &markdown, int left, int top, int width) { auto block = LaidOutBlock(); block.kind = PreparedBlockKind::DisplayMath; block.formulaIndex = prepared.formulaIndex; block.copyText = prepared.formulaTex; const auto &padding = markdown.displayMath.padding; const auto contentLeft = left + padding.left(); const auto contentTop = top + padding.top(); const auto contentWidth = std::max( width - padding.left() - padding.right(), 1); const auto formula = PreparedFormulaFor(formulas, prepared.formulaIndex); auto formulaWidth = 0; auto formulaHeight = 0; if (formula && formula->measured.success) { formulaWidth = std::max(formula->measured.logicalSize.width(), 1); formulaHeight = std::max(formula->measured.logicalSize.height(), 1); } else { const auto &fallbackPadding = markdown.displayMath.fallbackPadding; const auto fallbackPaddingWidth = fallbackPadding.left() + fallbackPadding.right(); const auto fallbackText = formula ? formula->measured.fallbackText : prepared.formulaTex.trimmed(); block.textWidth = std::max(contentWidth - fallbackPaddingWidth, 1); block.fallbackLeaf = Ui::Text::String( TextMinResizeWidth(block.textWidth)); block.fallbackLeaf.setMarkedText( markdown.displayMath.fallbackStyle, TextWithEntities::Simple(fallbackText), kIvMarkedTextOptions); block.textWidth = std::min( block.textWidth, std::max(block.fallbackLeaf.maxWidth(), 1)); auto textHeight = std::max( block.fallbackLeaf.countHeight(block.textWidth, true), TextLineHeight(markdown.displayMath.fallbackStyle)); formulaWidth = std::min( block.textWidth + fallbackPaddingWidth, contentWidth); block.textWidth = std::max(formulaWidth - fallbackPaddingWidth, 1); textHeight = std::max( block.fallbackLeaf.countHeight(block.textWidth, true), TextLineHeight(markdown.displayMath.fallbackStyle)); formulaHeight = fallbackPadding.top() + textHeight + fallbackPadding.bottom(); block.textRect.setSize(QSize(block.textWidth, textHeight)); } const auto centered = (markdown.displayMath.align == ::style::al_center) && (formulaWidth <= contentWidth); block.formulaAlign = centered ? ::style::al_center : ::style::al_left; block.contentRect = QRect( contentLeft, contentTop, contentWidth, formulaHeight); block.formulaRect = QRect( centered ? (contentLeft + ((contentWidth - formulaWidth) / 2)) : contentLeft, contentTop, formulaWidth, formulaHeight); block.visibleFormulaRect = block.formulaRect.intersected(block.contentRect); block.outer = QRect( left, top, std::max(width, 1), padding.top() + formulaHeight + padding.bottom()); block.overflowed = formula && formula->measured.success && (block.formulaRect.width() > block.visibleFormulaRect.width()); if (!(formula && formula->measured.success)) { const auto &fallbackPadding = markdown.displayMath.fallbackPadding; block.textRect.moveTo( block.formulaRect.x() + fallbackPadding.left(), block.formulaRect.y() + fallbackPadding.top()); block.firstLineBaseline = LeafFirstLineBaseline( block.fallbackLeaf, block.textRect, markdown.displayMath.fallbackStyle); } return block; } LaidOutBlock LayoutTableBlock( const PreparedBlock &prepared, std::vector *formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &markdown, int left, int top, int width) { auto block = LaidOutBlock(); block.kind = PreparedBlockKind::Table; const auto columnCount = prepared.tableColumnCount; if (!columnCount || prepared.tableRows.empty()) { block.outer = QRect(left, top, std::max(width, 1), 0); return block; } auto rows = std::vector(); rows.reserve(prepared.tableRows.size()); for (const auto &preparedRow : prepared.tableRows) { auto row = TableRowLayoutData(); row.header = preparedRow.header; row.cells.reserve(columnCount); for (const auto &preparedCell : preparedRow.cells) { row.cells.push_back(InitializeTableCellLayout( preparedCell, preparedRow.header, formulas, inlineFormulaObjects, mediaRuntime, markdown)); } rows.push_back(std::move(row)); } block.tableColumnWidths = ComputeTableColumnWidths( rows, columnCount, width, markdown, &block.overflowed); const auto &padding = markdown.table.cellPadding; const auto border = st::defaultTable.border; auto tableWidth = border; for (const auto columnWidth : block.tableColumnWidths) { tableWidth += columnWidth + border; } auto y = top + border; block.tableRows.reserve(rows.size()); for (auto &rowData : rows) { auto rowHeight = 0; auto textHeights = std::vector(columnCount, 0); for (auto column = 0; column != columnCount; ++column) { const auto &textStyle = TableCellTextStyle(rowData.header, markdown); const auto contentWidth = std::max( block.tableColumnWidths[column] - padding.left() - padding.right(), 1); rowData.cells[column].cell.textWidth = contentWidth; textHeights[column] = (contentWidth >= rowData.cells[column].preferredWidth) ? rowData.cells[column].preferredHeight : std::max( rowData.cells[column].cell.leaf.countHeight( contentWidth, true), TextLineHeight(textStyle)); rowHeight = std::max( rowHeight, textHeights[column] + padding.top() + padding.bottom()); } auto row = LaidOutTableRow(); row.header = rowData.header; row.outer = QRect( left + border, y, std::max(tableWidth - (2 * border), 0), rowHeight); auto x = left + border; row.cells.reserve(columnCount); for (auto column = 0; column != columnCount; ++column) { auto cell = std::move(rowData.cells[column].cell); const auto columnWidth = block.tableColumnWidths[column]; cell.outer = QRect(x, y, columnWidth, rowHeight); cell.textRect = QRect( x + padding.left(), y + padding.top(), cell.textWidth, textHeights[column]); row.cells.push_back(std::move(cell)); x += columnWidth + border; } block.tableRows.push_back(std::move(row)); y += rowHeight + border; } const auto tableHeight = std::max(y - top, border); block.tableRect = QRect(left, top, tableWidth, tableHeight); block.visibleTableRect = QRect( left, top, std::min(tableWidth, std::max(width, 1)), tableHeight); block.contentRect = block.visibleTableRect; block.outer = block.visibleTableRect; for (const auto &row : block.tableRows) { const auto &textStyle = TableCellTextStyle(row.header, markdown); for (const auto &cell : row.cells) { if (cell.leaf.isEmpty()) { continue; } block.firstLineBaseline = LeafFirstLineBaseline( cell.leaf, cell.textRect, textStyle); return block; } } return block; } LaidOutBlock LayoutPlaceholderBlock( const PreparedBlock &prepared, std::vector *formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &markdown, int left, int top, int width) { auto block = LaidOutBlock(); block.kind = PreparedBlockKind::Placeholder; block.anchorId = prepared.anchorId; block.copyText = prepared.placeholder.copyText; block.labelText = prepared.placeholder.label; const auto &style = markdown.placeholder; const auto blockWidth = std::max(width, 1); const auto contentLeft = left + style.padding.left(); const auto contentWidth = std::max( blockWidth - style.padding.left() - style.padding.right(), 1); block.labelWidth = contentWidth; block.labelLeaf = Ui::Text::String(TextMinResizeWidth(contentWidth)); block.labelLeaf.setMarkedText( style.labelStyle, TextWithEntities::Simple(block.labelText), kIvMarkedTextOptions); const auto labelHeight = std::max( block.labelLeaf.countHeight(contentWidth, true), TextLineHeight(style.labelStyle)); const auto mediaHeight = std::max( style.minHeight, labelHeight + style.padding.top() + style.padding.bottom()); block.mediaRect = QRect(left, top, blockWidth, mediaHeight); block.visibleMediaRect = block.mediaRect; block.labelRect = QRect( contentLeft, top + std::max((mediaHeight - labelHeight) / 2, 0), contentWidth, labelHeight); block.firstLineBaseline = LeafFirstLineBaseline( block.labelLeaf, block.labelRect, style.labelStyle); auto bottom = top + mediaHeight; LayoutMediaCaption( &block, prepared, formulas, inlineFormulaObjects, mediaRuntime, markdown, contentLeft, bottom, contentWidth, style.captionSkip, &bottom); block.contentRect = QRect( left, top, blockWidth, std::max(bottom - top, mediaHeight)); block.outer = block.contentRect; return block; } LaidOutBlock LayoutPhotoBlock( const PreparedBlock &prepared, std::vector *formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &markdown, int left, int top, int width) { auto block = LaidOutBlock(); block.kind = PreparedBlockKind::Photo; block.anchorId = prepared.anchorId; block.copyText = kPhotoCopyLabel; const auto &style = markdown.photo; const auto blockWidth = std::max(width, 1); const auto mediaLeft = left + style.padding.left(); const auto mediaTop = top + style.padding.top(); const auto mediaWidth = std::max( blockWidth - style.padding.left() - style.padding.right(), 1); const auto mediaHeight = MediaHeightForWidth( mediaWidth, prepared.photo.width, prepared.photo.height); block.mediaRect = QRect(mediaLeft, mediaTop, mediaWidth, mediaHeight); block.visibleMediaRect = block.mediaRect; if (mediaRuntime) { block.photoRuntime = mediaRuntime->resolvePhoto(prepared.photo.photoId); } ResolveRuntimeImages( block.photoRuntime, QSize(mediaWidth, mediaHeight), &block.thumbnailImage, &block.fullImage); if (!prepared.photo.urlOverride.isEmpty()) { block.activation.kind = MediaActivationKind::ExternalUrl; block.activation.url = prepared.photo.urlOverride; } else if (prepared.photo.viewerOpen && block.photoRuntime) { block.activation.kind = MediaActivationKind::Photo; block.activation.photo = block.photoRuntime; } auto bottom = mediaTop + mediaHeight + style.padding.bottom(); LayoutMediaCaption( &block, prepared, formulas, inlineFormulaObjects, mediaRuntime, markdown, mediaLeft, bottom, mediaWidth, style.captionSkip, &bottom); block.contentRect = QRect( mediaLeft, mediaTop, mediaWidth, std::max(bottom - mediaTop, mediaHeight)); block.outer = QRect(left, top, blockWidth, std::max(bottom - top, mediaHeight)); return block; } LaidOutBlock LayoutVideoBlock( const PreparedBlock &prepared, std::vector *formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &markdown, int left, int top, int width) { auto block = LaidOutBlock(); block.kind = PreparedBlockKind::Video; block.anchorId = prepared.anchorId; block.copyText = tr::lng_in_dlg_video(tr::now); const auto &style = markdown.photo; const auto blockWidth = std::max(width, 1); const auto mediaLeft = left + style.padding.left(); const auto mediaTop = top + style.padding.top(); const auto mediaWidth = std::max( blockWidth - style.padding.left() - style.padding.right(), 1); const auto mediaHeight = MediaHeightForWidth( mediaWidth, prepared.video.media.width, prepared.video.media.height); block.mediaRect = QRect(mediaLeft, mediaTop, mediaWidth, mediaHeight); block.visibleMediaRect = block.mediaRect; if (mediaRuntime) { block.documentRuntime = mediaRuntime->resolveDocument( prepared.video.media.id); } ResolveRuntimeImages( block.documentRuntime, QSize(mediaWidth, mediaHeight), &block.thumbnailImage, &block.fullImage); if (block.documentRuntime) { block.activation.kind = MediaActivationKind::Document; block.activation.document = block.documentRuntime; } auto bottom = mediaTop + mediaHeight + style.padding.bottom(); LayoutMediaCaption( &block, prepared, formulas, inlineFormulaObjects, mediaRuntime, markdown, mediaLeft, bottom, mediaWidth, style.captionSkip, &bottom); block.contentRect = QRect( mediaLeft, mediaTop, mediaWidth, std::max(bottom - mediaTop, mediaHeight)); block.outer = QRect(left, top, blockWidth, std::max(bottom - top, mediaHeight)); return block; } LaidOutBlock LayoutAudioBlock( const PreparedBlock &prepared, std::vector *formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &markdown, int left, int top, int width) { auto block = LaidOutBlock(); block.kind = PreparedBlockKind::Audio; block.anchorId = prepared.anchorId; block.labelText = AudioTitleText(prepared.audio); block.copyText = AudioCopyText(prepared.audio); const auto &card = markdown.audio; const auto &padding = card.padding; const auto &titleStyle = card.titleStyle; const auto &subtitleStyle = card.subtitleStyle; const auto subtitleText = AudioSubtitleText(prepared.audio); const auto blockWidth = std::max(width, 1); const auto contentLeft = left + padding.left(); const auto contentWidth = std::max( blockWidth - padding.left() - padding.right(), 1); block.labelWidth = contentWidth; SetPlainTextLeaf( &block.labelLeaf, titleStyle, block.labelText, block.labelWidth); const auto titleHeight = LeafHeight( block.labelLeaf, titleStyle, block.labelWidth); auto subtitleHeight = 0; if (!subtitleText.isEmpty()) { block.subtitleWidth = contentWidth; SetPlainTextLeaf( &block.subtitleLeaf, subtitleStyle, subtitleText, block.subtitleWidth); subtitleHeight = LeafHeight( block.subtitleLeaf, subtitleStyle, block.subtitleWidth); } const auto textSkip = subtitleHeight ? card.textSkip : 0; const auto textHeight = titleHeight + textSkip + subtitleHeight; const auto cardHeight = padding.top() + textHeight + padding.bottom(); block.mediaRect = QRect(left, top, blockWidth, cardHeight); block.visibleMediaRect = block.mediaRect; block.labelRect = QRect( contentLeft, top + padding.top(), block.labelWidth, titleHeight); if (subtitleHeight) { block.subtitleRect = QRect( contentLeft, block.labelRect.y() + block.labelRect.height() + textSkip, block.subtitleWidth, subtitleHeight); } block.firstLineBaseline = LeafFirstLineBaseline( block.labelLeaf, block.labelRect, titleStyle); if (mediaRuntime) { block.documentRuntime = mediaRuntime->resolveDocument( prepared.audio.documentId); } if (block.documentRuntime) { block.activation.kind = MediaActivationKind::Document; block.activation.document = block.documentRuntime; } auto bottom = top + cardHeight; LayoutMediaCaption( &block, prepared, formulas, inlineFormulaObjects, mediaRuntime, markdown, contentLeft, bottom, contentWidth, card.captionSkip, &bottom); block.contentRect = QRect(left, top, blockWidth, std::max(bottom - top, cardHeight)); block.outer = block.contentRect; return block; } LaidOutBlock LayoutMapBlock( const PreparedBlock &prepared, std::vector *formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &markdown, int left, int top, int width) { auto block = LaidOutBlock(); block.kind = PreparedBlockKind::Map; block.anchorId = prepared.anchorId; block.copyText = tr::lng_maps_point(tr::now); const auto &style = markdown.photo; const auto blockWidth = std::max(width, 1); const auto mediaLeft = left + style.padding.left(); const auto mediaTop = top + style.padding.top(); const auto mediaWidth = std::max( blockWidth - style.padding.left() - style.padding.right(), 1); const auto mediaHeight = MediaHeightForWidth( mediaWidth, prepared.map.width, prepared.map.height); block.mediaRect = QRect(mediaLeft, mediaTop, mediaWidth, mediaHeight); block.visibleMediaRect = block.mediaRect; if (mediaRuntime) { block.mapRuntime = mediaRuntime->resolveMap( prepared.map.latitude, prepared.map.longitude, prepared.map.accessHash, QSize(mediaWidth, mediaHeight), prepared.map.zoom); } ResolveRuntimeImages( block.mapRuntime, QSize(mediaWidth, mediaHeight), &block.thumbnailImage, &block.fullImage); if (!prepared.map.url.isEmpty()) { block.activation.kind = MediaActivationKind::ExternalUrl; block.activation.url = prepared.map.url; } auto bottom = mediaTop + mediaHeight + style.padding.bottom(); LayoutMediaCaption( &block, prepared, formulas, inlineFormulaObjects, mediaRuntime, markdown, mediaLeft, bottom, mediaWidth, style.captionSkip, &bottom); block.contentRect = QRect( mediaLeft, mediaTop, mediaWidth, std::max(bottom - mediaTop, mediaHeight)); block.outer = QRect(left, top, blockWidth, std::max(bottom - top, mediaHeight)); return block; } LaidOutBlock LayoutChannelBlock( const PreparedBlock &prepared, std::vector *formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &markdown, int left, int top, int width) { auto block = LaidOutBlock(); block.kind = PreparedBlockKind::Channel; block.anchorId = prepared.anchorId; block.labelText = prepared.channel.title; block.copyText = ChannelCopyText(prepared.channel); const auto &card = markdown.channel; const auto &padding = card.padding; const auto &button = card.button; const auto &buttonPadding = button.padding; const auto &titleStyle = card.titleStyle; const auto &subtitleStyle = card.subtitleStyle; const auto &actionStyle = button.textStyle; const auto subtitleText = ChannelSubtitleText(prepared.channel); const auto blockWidth = std::max(width, 1); const auto contentLeft = left + padding.left(); const auto contentWidth = std::max( blockWidth - padding.left() - padding.right(), 1); if (mediaRuntime) { block.channelRuntime = mediaRuntime->resolveChannel( prepared.channel.channelId, prepared.channel.username); } const auto joinVisible = block.channelRuntime && block.channelRuntime->joinVisible(); if (block.channelRuntime) { block.activation.kind = MediaActivationKind::OpenChannel; block.activation.channel = block.channelRuntime; } if (joinVisible) { block.actionActivation.kind = MediaActivationKind::JoinChannel; block.actionActivation.channel = block.channelRuntime; } auto actionTextHeight = 0; auto actionOuterWidth = 0; auto actionOuterHeight = 0; if (joinVisible) { SetPlainTextLeaf( &block.actionLeaf, actionStyle, tr::lng_iv_join_channel(tr::now), contentWidth); block.actionWidth = std::max(block.actionLeaf.maxWidth(), 1); actionTextHeight = LeafHeight( block.actionLeaf, actionStyle, block.actionWidth); actionOuterWidth = block.actionWidth + buttonPadding.left() + buttonPadding.right(); actionOuterHeight = actionTextHeight + buttonPadding.top() + buttonPadding.bottom(); } block.labelWidth = std::max( contentWidth - (joinVisible ? (actionOuterWidth + card.buttonSkip) : 0), 1); SetPlainTextLeaf( &block.labelLeaf, titleStyle, block.labelText, block.labelWidth); const auto titleHeight = LeafHeight( block.labelLeaf, titleStyle, block.labelWidth); auto subtitleHeight = 0; if (!subtitleText.isEmpty()) { block.subtitleWidth = block.labelWidth; SetPlainTextLeaf( &block.subtitleLeaf, subtitleStyle, subtitleText, block.subtitleWidth); subtitleHeight = LeafHeight( block.subtitleLeaf, subtitleStyle, block.subtitleWidth); } const auto textSkip = subtitleHeight ? card.textSkip : 0; const auto textHeight = titleHeight + textSkip + subtitleHeight; const auto cardContentHeight = std::max(textHeight, actionOuterHeight); const auto cardHeight = padding.top() + cardContentHeight + padding.bottom(); block.mediaRect = QRect(left, top, blockWidth, cardHeight); block.visibleMediaRect = block.mediaRect; const auto textTop = top + padding.top() + std::max((cardContentHeight - textHeight) / 2, 0); block.labelRect = QRect( contentLeft, textTop, block.labelWidth, titleHeight); if (subtitleHeight) { block.subtitleRect = QRect( contentLeft, block.labelRect.y() + block.labelRect.height() + textSkip, block.subtitleWidth, subtitleHeight); } if (joinVisible) { block.actionRect = QRect( left + blockWidth - padding.right() - actionOuterWidth, top + padding.top() + std::max((cardContentHeight - actionOuterHeight) / 2, 0), actionOuterWidth, actionOuterHeight); } block.firstLineBaseline = LeafFirstLineBaseline( block.labelLeaf, block.labelRect, titleStyle); block.contentRect = block.mediaRect; block.outer = block.mediaRect; return block; } LaidOutBlock LayoutGroupedMediaBlock( const PreparedBlock &prepared, const std::vector *formulas, InlineFormulaObjectCache *inlineFormulaObjects, const std::shared_ptr &mediaRuntime, const style::Markdown &markdown, int left, int top, int width) { auto block = LaidOutBlock(); block.kind = PreparedBlockKind::GroupedMedia; block.anchorId = prepared.anchorId; block.copyText = GroupedMediaCopyText(prepared.groupedMedia); const auto &style = markdown.groupedMedia; const auto blockWidth = std::max(width, 1); const auto mediaLeft = left + style.padding.left(); const auto mediaTop = top + style.padding.top(); const auto mediaWidth = std::max( blockWidth - style.padding.left() - style.padding.right(), 1); auto sizes = std::vector(); sizes.reserve(prepared.groupedMedia.items.size()); for (const auto &item : prepared.groupedMedia.items) { sizes.push_back(QSize( std::max(item.media.width, 1), std::max(item.media.height, 1))); } auto layout = Ui::LayoutMediaGroup( sizes, mediaWidth, GroupedMediaMinWidth(mediaWidth, style.itemSkip), style.itemSkip); block.groupedMediaItems.reserve(layout.size()); for (auto i = 0, count = std::min( int(layout.size()), int(prepared.groupedMedia.items.size())); i != count; ++i) { auto item = LaidOutGroupedMediaItem(); ResolveGroupedMediaItemLayout( &item, prepared.groupedMedia.items[i], mediaRuntime, layout[i].geometry.translated(mediaLeft, mediaTop)); block.groupedMediaItems.push_back(std::move(item)); } const auto mediaHeight = GroupedMediaLayoutHeight(layout); const auto laidOutWidth = GroupedMediaLayoutWidth(layout); block.mediaRect = QRect( mediaLeft, mediaTop, std::max(laidOutWidth, 1), std::max(mediaHeight, 1)); block.visibleMediaRect = block.mediaRect; auto bottom = block.mediaRect.y() + block.mediaRect.height() + style.padding.bottom(); LayoutMediaCaption( &block, prepared, formulas, inlineFormulaObjects, mediaRuntime, markdown, mediaLeft, bottom, block.mediaRect.width(), style.captionSkip, &bottom); block.contentRect = QRect( mediaLeft, mediaTop, block.mediaRect.width(), std::max(bottom - mediaTop, block.mediaRect.height())); block.outer = QRect( left, top, blockWidth, std::max(bottom - top, block.mediaRect.height())); return block; } } // namespace Iv::Markdown