mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-06 11:52:58 +00:00
4750 lines
128 KiB
C++
4750 lines
128 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_blocks.h"
|
|
#include "iv/markdown/iv_markdown_media_block.h"
|
|
#include "iv/markdown/iv_markdown_article_text.h"
|
|
#include "iv/markdown/iv_markdown_prepare_links.h"
|
|
#include "iv/markdown/iv_markdown_prepare_serialize.h"
|
|
#include "spellcheck/spellcheck_highlight_syntax.h"
|
|
|
|
#include "lang/lang_keys.h"
|
|
#include "styles/style_iv.h"
|
|
#include "styles/style_widgets.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <utility>
|
|
|
|
namespace Iv::Markdown {
|
|
namespace {
|
|
|
|
constexpr auto kIvMarkedTextOptions = TextParseOptions{
|
|
TextParseMultiline,
|
|
0,
|
|
0,
|
|
Qt::LayoutDirectionAuto,
|
|
};
|
|
|
|
constexpr auto kCodeTabColumns = 4;
|
|
constexpr auto kCodeTrailingGuard = 0x2060;
|
|
constexpr auto kReadableTextColumns = 12;
|
|
constexpr auto kReadableTextLineHeights = 4;
|
|
constexpr auto kReadableCodeColumns = 16;
|
|
|
|
thread_local const LayoutContext *CurrentLayoutContext = nullptr;
|
|
|
|
[[nodiscard]] QString DetailsStateText(bool open) {
|
|
return open
|
|
? tr::lng_iv_details_state_expanded(tr::now)
|
|
: tr::lng_iv_details_state_collapsed(tr::now);
|
|
}
|
|
|
|
[[nodiscard]] size_t CombineHash(size_t accumulator, size_t value) {
|
|
return (accumulator * 1315423911U) ^ value;
|
|
}
|
|
|
|
[[nodiscard]] size_t HashPreparedEditBlockPath(
|
|
const PreparedEditBlockPath &value) {
|
|
auto result = CombineHash(0, size_t(value.container.steps.size() + 1));
|
|
for (const auto &step : value.container.steps) {
|
|
result = CombineHash(result, size_t(step.kind));
|
|
result = CombineHash(result, size_t(step.blockIndex + 1));
|
|
result = CombineHash(result, size_t(step.listItemIndex + 1));
|
|
}
|
|
return CombineHash(result, size_t(value.index + 1));
|
|
}
|
|
|
|
[[nodiscard]] size_t HashPreparedEditBlockSource(
|
|
const PreparedEditBlockSource &value) {
|
|
return HashPreparedEditBlockPath(value.path);
|
|
}
|
|
|
|
[[nodiscard]] size_t HashPreparedEditListItemSource(
|
|
const PreparedEditListItemSource &value) {
|
|
auto result = HashPreparedEditBlockPath(value.block);
|
|
return CombineHash(result, size_t(value.listItemIndex + 1));
|
|
}
|
|
|
|
[[nodiscard]] size_t HashPreparedEditTableCellSource(
|
|
const PreparedEditTableCellSource &value) {
|
|
auto result = HashPreparedEditBlockPath(value.block);
|
|
result = CombineHash(result, size_t(value.tableRowIndex + 1));
|
|
result = CombineHash(result, size_t(value.tableCellIndex + 1));
|
|
result = CombineHash(result, size_t(value.column + 1));
|
|
result = CombineHash(result, size_t(value.colspan + 1));
|
|
return CombineHash(result, size_t(value.rowspan + 1));
|
|
}
|
|
|
|
[[nodiscard]] size_t HashPreparedEditLeafSource(
|
|
const PreparedEditLeafSource &value) {
|
|
auto result = CombineHash(0, size_t(value.kind));
|
|
result = CombineHash(result, HashPreparedEditBlockPath(value.block));
|
|
result = CombineHash(result, size_t(value.listItemIndex + 1));
|
|
result = CombineHash(result, size_t(value.tableRowIndex + 1));
|
|
return CombineHash(result, size_t(value.tableCellIndex + 1));
|
|
}
|
|
|
|
[[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(
|
|
const PreparedTableCell &prepared,
|
|
const style::Markdown &st) {
|
|
if (prepared.header) {
|
|
return st.table.headerStyle;
|
|
}
|
|
return st.table.bodyStyle;
|
|
}
|
|
|
|
[[nodiscard]] const style::TextStyle &TableCellTextStyle(
|
|
const LaidOutTableCell &cell,
|
|
const style::Markdown &st) {
|
|
if (cell.header) {
|
|
return st.table.headerStyle;
|
|
}
|
|
return st.table.bodyStyle;
|
|
}
|
|
|
|
[[nodiscard]] int TableBorder(
|
|
bool bordered,
|
|
const style::Markdown &st) {
|
|
return bordered ? st.table.border : 0;
|
|
}
|
|
|
|
[[nodiscard]] bool TextDependsOnMediaRuntime(
|
|
const TextWithEntities &text) {
|
|
for (const auto &entity : text.entities) {
|
|
if (entity.type() != EntityType::CustomEmoji) {
|
|
continue;
|
|
}
|
|
const auto parsed = ParseInlineTextObjectEntity(entity.data());
|
|
if (parsed && (parsed->kind == InlineTextObjectKind::IvImage)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
[[nodiscard]] TextWithEntities DisplayMathFallbackText() {
|
|
auto result = TextWithEntities::Simple(u"Invalid formula"_q);
|
|
result.entities.push_back(EntityInText(
|
|
EntityType::Italic,
|
|
0,
|
|
result.text.size()));
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] size_t TextStyleKey(const style::TextStyle &style) {
|
|
return reinterpret_cast<size_t>(&style);
|
|
}
|
|
|
|
void SetPlainTextLeaf(
|
|
Ui::Text::String *leaf,
|
|
const style::TextStyle &textStyle,
|
|
const QString &text,
|
|
int minResizeWidth);
|
|
|
|
[[nodiscard]] CachedTextLeafSourceSignature MarkedTextLeafSourceSignature(
|
|
TextWithEntities text,
|
|
const style::TextStyle &textStyle,
|
|
int minResizeWidth) {
|
|
auto result = CachedTextLeafSourceSignature();
|
|
result.dependsOnMediaRuntime = TextDependsOnMediaRuntime(text);
|
|
result.text = std::move(text);
|
|
result.minResizeWidth = minResizeWidth;
|
|
result.styleKey = TextStyleKey(textStyle);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] CachedTextLeafSourceSignature PlainTextLeafSourceSignature(
|
|
const QString &text,
|
|
const style::TextStyle &textStyle,
|
|
int minResizeWidth) {
|
|
return MarkedTextLeafSourceSignature(
|
|
TextWithEntities::Simple(text),
|
|
textStyle,
|
|
minResizeWidth);
|
|
}
|
|
|
|
[[nodiscard]] CachedTextLeafSourceSignature CodeTextLeafSourceSignature(
|
|
const PreparedBlock &prepared,
|
|
const style::Markdown &st) {
|
|
auto result = MarkedTextLeafSourceSignature(
|
|
CodeBlockDisplayText(prepared.text),
|
|
st.code,
|
|
CodeTextMinResizeWidth(st));
|
|
result.codeLanguage = prepared.codeLanguage;
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] CachedTextLeafKey BlockCachedTextLeafKey(
|
|
CachedTextLeafSlot slot,
|
|
const PreparedBlock &prepared,
|
|
const std::vector<int> &preparedPath) {
|
|
auto result = CachedTextLeafKey();
|
|
result.slot = slot;
|
|
if ((slot == CachedTextLeafSlot::Leaf
|
|
|| slot == CachedTextLeafSlot::Placeholder
|
|
|| slot == CachedTextLeafSlot::Fallback)
|
|
&& prepared.editLeaf) {
|
|
result.identityKind = CachedTextLeafIdentityKind::EditLeaf;
|
|
result.editLeaf = *prepared.editLeaf;
|
|
return result;
|
|
}
|
|
if ((slot == CachedTextLeafSlot::Marker) && prepared.editListItem) {
|
|
result.identityKind = CachedTextLeafIdentityKind::EditListItem;
|
|
result.editListItem = *prepared.editListItem;
|
|
return result;
|
|
}
|
|
if (prepared.editBlock) {
|
|
result.identityKind = CachedTextLeafIdentityKind::EditBlock;
|
|
result.editBlock = *prepared.editBlock;
|
|
return result;
|
|
}
|
|
if (prepared.editListItem) {
|
|
result.identityKind = CachedTextLeafIdentityKind::EditListItem;
|
|
result.editListItem = *prepared.editListItem;
|
|
return result;
|
|
}
|
|
if (prepared.editLeaf) {
|
|
result.identityKind = CachedTextLeafIdentityKind::EditLeaf;
|
|
result.editLeaf = *prepared.editLeaf;
|
|
return result;
|
|
}
|
|
result.preparedPath = preparedPath;
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] CachedTextLeafKey TableCellCachedTextLeafKey(
|
|
CachedTextLeafSlot slot,
|
|
const PreparedTableCell &prepared,
|
|
const std::vector<int> &preparedPath,
|
|
int tableRowIndex,
|
|
int tableCellIndex) {
|
|
auto result = CachedTextLeafKey();
|
|
result.slot = slot;
|
|
result.tableRowIndex = tableRowIndex;
|
|
result.tableCellIndex = tableCellIndex;
|
|
if (prepared.editLeaf) {
|
|
result.identityKind = CachedTextLeafIdentityKind::EditLeaf;
|
|
result.editLeaf = *prepared.editLeaf;
|
|
return result;
|
|
}
|
|
if (prepared.editCell) {
|
|
result.identityKind = CachedTextLeafIdentityKind::EditTableCell;
|
|
result.editTableCell = *prepared.editCell;
|
|
return result;
|
|
}
|
|
result.preparedPath = preparedPath;
|
|
return result;
|
|
}
|
|
|
|
template <typename Builder, typename Consumer>
|
|
auto WithCachedTextLeaf(
|
|
LayoutContext context,
|
|
CachedTextLeafKey key,
|
|
CachedTextLeafSourceSignature source,
|
|
Builder &&builder,
|
|
Consumer &&consumer) {
|
|
if (const auto pool = context.cachedTextLeafs) {
|
|
auto i = pool->entries.find(key);
|
|
if (i == end(pool->entries)
|
|
|| (i->second.source != source)
|
|
|| i->second.leaf.isEmpty()) {
|
|
auto entry = CachedTextLeafEntry();
|
|
entry.source = source;
|
|
builder(&entry.leaf, &entry.syntaxHighlightProcessId);
|
|
i = pool->entries.insert_or_assign(
|
|
std::move(key),
|
|
std::move(entry)).first;
|
|
}
|
|
return consumer(i->second.leaf, i->second.syntaxHighlightProcessId);
|
|
}
|
|
auto leaf = Ui::Text::String();
|
|
auto syntaxHighlightProcessId = Spellchecker::HighlightProcessId(0);
|
|
builder(&leaf, &syntaxHighlightProcessId);
|
|
return consumer(leaf, syntaxHighlightProcessId);
|
|
}
|
|
|
|
template <typename Builder>
|
|
void BuildOrReuseCachedTextLeaf(
|
|
Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId *syntaxHighlightProcessId,
|
|
LayoutContext context,
|
|
CachedTextLeafKey key,
|
|
const CachedTextLeafSourceSignature &source,
|
|
Builder &&builder) {
|
|
if (const auto pool = context.cachedTextLeafs) {
|
|
if (const auto i = pool->entries.find(key);
|
|
i != end(pool->entries)
|
|
&& (i->second.source == source)
|
|
&& !i->second.leaf.isEmpty()) {
|
|
if (syntaxHighlightProcessId) {
|
|
*syntaxHighlightProcessId = i->second.syntaxHighlightProcessId;
|
|
}
|
|
*leaf = std::move(i->second.leaf);
|
|
pool->entries.erase(i);
|
|
SetTextLeafSpoilerLinkFilter(leaf, context.spoilerLinkFilter);
|
|
return;
|
|
}
|
|
}
|
|
builder(leaf, syntaxHighlightProcessId);
|
|
SetTextLeafSpoilerLinkFilter(leaf, context.spoilerLinkFilter);
|
|
}
|
|
|
|
[[nodiscard]] LaidOutTableCell InitializeTableCellLayout(
|
|
const PreparedTableCell &prepared,
|
|
const std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
int tableRowIndex,
|
|
int tableCellIndex,
|
|
LayoutContext context) {
|
|
auto result = LaidOutTableCell();
|
|
const auto &textStyle = TableCellTextStyle(prepared, st);
|
|
const auto minResizeWidth = TableCellTextMinResizeWidth(textStyle, st);
|
|
result.header = prepared.header;
|
|
result.verticalAlignment = prepared.verticalAlignment;
|
|
result.align = CellAlign(prepared.alignment);
|
|
result.column = std::max(prepared.column, 0);
|
|
result.colspan = std::max(prepared.colspan, 1);
|
|
result.rowspan = std::max(prepared.rowspan, 1);
|
|
result.editCell = prepared.editCell;
|
|
result.editLeaf = prepared.editLeaf;
|
|
BuildOrReuseCachedTextLeaf(
|
|
&result.leaf,
|
|
nullptr,
|
|
context,
|
|
TableCellCachedTextLeafKey(
|
|
CachedTextLeafSlot::TableCellText,
|
|
prepared,
|
|
context.preparedPath,
|
|
tableRowIndex,
|
|
tableCellIndex),
|
|
MarkedTextLeafSourceSignature(prepared.text, textStyle, minResizeWidth),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetTextLeaf(
|
|
leaf,
|
|
textStyle,
|
|
st,
|
|
prepared.text,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
minResizeWidth,
|
|
context.repaint,
|
|
context.repaintRect);
|
|
BindLinks(leaf, prepared.links);
|
|
});
|
|
BindLinks(&result.leaf, prepared.links);
|
|
const auto usePlaceholder = prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty();
|
|
if (usePlaceholder) {
|
|
result.placeholderText = prepared.editPlaceholderText;
|
|
BuildOrReuseCachedTextLeaf(
|
|
&result.placeholderLeaf,
|
|
nullptr,
|
|
context,
|
|
TableCellCachedTextLeafKey(
|
|
CachedTextLeafSlot::TableCellPlaceholder,
|
|
prepared,
|
|
context.preparedPath,
|
|
tableRowIndex,
|
|
tableCellIndex),
|
|
PlainTextLeafSourceSignature(
|
|
result.placeholderText,
|
|
textStyle,
|
|
minResizeWidth),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetPlainTextLeaf(
|
|
leaf,
|
|
textStyle,
|
|
result.placeholderText,
|
|
minResizeWidth);
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] int TableSpanWidth(
|
|
const std::vector<int> &columnWidths,
|
|
int column,
|
|
int colspan,
|
|
int border) {
|
|
const auto from = std::clamp(column, 0, int(columnWidths.size()));
|
|
const auto to = std::clamp(column + colspan, 0, int(columnWidths.size()));
|
|
if (from >= to) {
|
|
return 0;
|
|
}
|
|
auto result = 0;
|
|
for (auto current = from; current != to; ++current) {
|
|
result += columnWidths[current];
|
|
}
|
|
return result + std::max(to - from - 1, 0) * border;
|
|
}
|
|
|
|
[[nodiscard]] int TableSpanHeight(
|
|
const std::vector<int> &rowHeights,
|
|
int row,
|
|
int rowspan,
|
|
int border) {
|
|
const auto from = std::clamp(row, 0, int(rowHeights.size()));
|
|
const auto to = std::clamp(row + rowspan, 0, int(rowHeights.size()));
|
|
if (from >= to) {
|
|
return 0;
|
|
}
|
|
auto result = 0;
|
|
for (auto current = from; current != to; ++current) {
|
|
result += rowHeights[current];
|
|
}
|
|
return result + std::max(to - from - 1, 0) * border;
|
|
}
|
|
|
|
void DistributeSizeDeficits(
|
|
std::vector<int> *sizes,
|
|
std::vector<int> deficits,
|
|
int *extra) {
|
|
auto remaining = 0;
|
|
for (const auto deficit : deficits) {
|
|
remaining += std::max(deficit, 0);
|
|
}
|
|
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 i = 0, count = int(deficits.size()); i != count && *extra > 0; ++i) {
|
|
if (deficits[i] <= 0) {
|
|
continue;
|
|
}
|
|
const auto delta = std::min({ deficits[i], step, *extra });
|
|
(*sizes)[i] += delta;
|
|
deficits[i] -= delta;
|
|
remaining -= delta;
|
|
*extra -= delta;
|
|
}
|
|
}
|
|
}
|
|
|
|
void DistributeSpanDelta(
|
|
std::vector<int> *sizes,
|
|
int from,
|
|
int to,
|
|
int delta) {
|
|
from = std::clamp(from, 0, int(sizes->size()));
|
|
to = std::clamp(to, 0, int(sizes->size()));
|
|
while (delta > 0 && from < to) {
|
|
const auto active = to - from;
|
|
const auto step = std::max(delta / active, 1);
|
|
for (auto i = from; i != to && delta > 0; ++i) {
|
|
const auto current = std::min(step, delta);
|
|
(*sizes)[i] += current;
|
|
delta -= current;
|
|
}
|
|
}
|
|
}
|
|
|
|
struct TableCellGeometryData {
|
|
LaidOutTableCell *cell = nullptr;
|
|
int minimumWidth = 0;
|
|
int preferredWidth = 0;
|
|
int preferredHeight = 0;
|
|
int textHeight = 0;
|
|
bool usePlaceholder = false;
|
|
};
|
|
|
|
struct TableRowGeometryData {
|
|
std::vector<TableCellGeometryData> cells;
|
|
bool header = false;
|
|
};
|
|
|
|
struct TableSpannedCellGeometryData {
|
|
int row = 0;
|
|
TableCellGeometryData *cell = nullptr;
|
|
};
|
|
|
|
[[nodiscard]] std::vector<int> ComputeTableColumnWidths(
|
|
std::vector<TableRowGeometryData> &rows,
|
|
int columnCount,
|
|
int width,
|
|
const style::Markdown &st,
|
|
bool bordered,
|
|
bool *overflowed) {
|
|
const auto &padding = st.table.cellPadding;
|
|
const auto border = TableBorder(bordered, st);
|
|
const auto paddingWidth = padding.left() + padding.right();
|
|
auto constraints = std::vector<TableCellMinimumWidthConstraint>();
|
|
for (auto &row : rows) {
|
|
for (auto &cellData : row.cells) {
|
|
if (!cellData.cell || cellData.minimumWidth <= 0) {
|
|
continue;
|
|
}
|
|
constraints.push_back({
|
|
.column = cellData.cell->column,
|
|
.colspan = cellData.cell->colspan,
|
|
.minimumWidth = std::max(
|
|
cellData.minimumWidth + paddingWidth,
|
|
st.table.minColumnWidth),
|
|
});
|
|
}
|
|
}
|
|
auto result = ComputeTableColumnMinimumWidths(
|
|
std::move(constraints),
|
|
columnCount,
|
|
st,
|
|
bordered);
|
|
auto singleColumnDeficits = std::vector<int>(std::max(columnCount, 0), 0);
|
|
auto spannedCells = std::vector<TableSpannedCellGeometryData>();
|
|
for (auto row = 0, rowCount = int(rows.size()); row != rowCount; ++row) {
|
|
for (auto &cellData : rows[row].cells) {
|
|
if (!cellData.cell) {
|
|
continue;
|
|
}
|
|
const auto from = std::clamp(cellData.cell->column, 0, columnCount);
|
|
const auto to = std::clamp(
|
|
cellData.cell->column + cellData.cell->colspan,
|
|
0,
|
|
columnCount);
|
|
if (from >= to) {
|
|
continue;
|
|
}
|
|
const auto preferredWidth = cellData.preferredWidth
|
|
+ padding.left()
|
|
+ padding.right();
|
|
if ((to - from) == 1) {
|
|
singleColumnDeficits[from] = std::max(
|
|
singleColumnDeficits[from],
|
|
preferredWidth - result[from]);
|
|
} else {
|
|
spannedCells.push_back({ row, &cellData });
|
|
}
|
|
}
|
|
}
|
|
|
|
const auto availableWidth = std::max(width, 1);
|
|
const auto minimumGridWidth = (columnCount > 0)
|
|
? TableGridWidth(result, st, bordered)
|
|
: TableMinimumGridWidth(columnCount, st, bordered);
|
|
*overflowed = (minimumGridWidth > availableWidth);
|
|
if (*overflowed || !columnCount) {
|
|
return result;
|
|
}
|
|
|
|
auto extra = availableWidth - minimumGridWidth;
|
|
DistributeSizeDeficits(&result, std::move(singleColumnDeficits), &extra);
|
|
std::sort(
|
|
spannedCells.begin(),
|
|
spannedCells.end(),
|
|
[](const TableSpannedCellGeometryData &a,
|
|
const TableSpannedCellGeometryData &b) {
|
|
const auto aSpan = a.cell ? a.cell->cell->colspan : 0;
|
|
const auto bSpan = b.cell ? b.cell->cell->colspan : 0;
|
|
return (aSpan < bSpan)
|
|
|| ((aSpan == bSpan) && (a.row < b.row))
|
|
|| ((aSpan == bSpan)
|
|
&& (a.row == b.row)
|
|
&& a.cell
|
|
&& b.cell
|
|
&& (a.cell->cell->column < b.cell->cell->column));
|
|
});
|
|
for (const auto &spanned : spannedCells) {
|
|
if (!spanned.cell || !spanned.cell->cell || extra <= 0) {
|
|
break;
|
|
}
|
|
const auto from = std::clamp(spanned.cell->cell->column, 0, columnCount);
|
|
const auto to = std::clamp(
|
|
spanned.cell->cell->column + spanned.cell->cell->colspan,
|
|
0,
|
|
columnCount);
|
|
if (from >= to) {
|
|
continue;
|
|
}
|
|
const auto preferredWidth = spanned.cell->preferredWidth
|
|
+ padding.left()
|
|
+ padding.right();
|
|
const auto currentWidth = TableSpanWidth(
|
|
result,
|
|
from,
|
|
to - from,
|
|
border);
|
|
const auto delta = std::min(
|
|
std::max(preferredWidth - currentWidth, 0),
|
|
extra);
|
|
DistributeSpanDelta(&result, from, to, 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;
|
|
}
|
|
|
|
void SetPlainTextLeaf(
|
|
Ui::Text::String *leaf,
|
|
const style::TextStyle &textStyle,
|
|
const QString &text,
|
|
int minResizeWidth) {
|
|
*leaf = Ui::Text::String(TextMinResizeWidth(minResizeWidth));
|
|
leaf->setMarkedText(
|
|
textStyle,
|
|
TextWithEntities::Simple(text),
|
|
kIvMarkedTextOptions);
|
|
}
|
|
|
|
void PopulateCodeBlockLeaf(
|
|
Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId *syntaxHighlightProcessId,
|
|
const TextWithEntities &codeText,
|
|
const std::vector<PreparedLink> &codeLinks,
|
|
const QString &codeLanguage,
|
|
const std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
bool allowAsyncSyntaxHighlighting,
|
|
CodeBlockSyntaxHighlightTracker *syntaxHighlightTracker,
|
|
Fn<void()> repaint,
|
|
Fn<void(QRect)> repaintRect,
|
|
Fn<bool(const ClickContext&)> spoilerLinkFilter) {
|
|
auto display = CodeBlockDisplayText(codeText);
|
|
auto highlightRequest = TextWithEntities();
|
|
highlightRequest.text = display.text;
|
|
if (!highlightRequest.text.isEmpty()) {
|
|
highlightRequest.entities.push_back(EntityInText(
|
|
EntityType::Pre,
|
|
0,
|
|
highlightRequest.text.size(),
|
|
codeLanguage));
|
|
}
|
|
const auto processId = allowAsyncSyntaxHighlighting
|
|
? (syntaxHighlightTracker
|
|
? syntaxHighlightTracker->tryHighlightSyntax(
|
|
highlightRequest.text,
|
|
codeLanguage,
|
|
highlightRequest)
|
|
: Spellchecker::TryHighlightSyntax(highlightRequest))
|
|
: 0;
|
|
for (const auto &entity : highlightRequest.entities) {
|
|
if (entity.type() == EntityType::Colorized
|
|
&& entity.length() > 0) {
|
|
display.entities.push_back(entity);
|
|
}
|
|
}
|
|
SortEntities(&display);
|
|
SetTextLeaf(
|
|
leaf,
|
|
st.code,
|
|
st,
|
|
display,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
CodeTextMinResizeWidth(st),
|
|
std::move(repaint),
|
|
std::move(repaintRect),
|
|
std::move(spoilerLinkFilter));
|
|
BindLinks(leaf, codeLinks);
|
|
if (syntaxHighlightProcessId) {
|
|
*syntaxHighlightProcessId = processId;
|
|
}
|
|
}
|
|
|
|
[[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 LeafHeightWithLineLimit(
|
|
const Ui::Text::String &leaf,
|
|
const style::TextStyle &textStyle,
|
|
int width,
|
|
int lines) {
|
|
auto result = LeafHeight(leaf, textStyle, width);
|
|
if (lines > 0) {
|
|
result = std::min(result, lines * TextLineHeight(textStyle));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] QRect PaddedBand(
|
|
int left,
|
|
int width,
|
|
QMargins padding) {
|
|
const auto paddedLeft = left + padding.left();
|
|
const auto paddedWidth = std::max(
|
|
width - padding.left() - padding.right(),
|
|
1);
|
|
return QRect(paddedLeft, 0, paddedWidth, 0);
|
|
}
|
|
|
|
[[nodiscard]] QRect ArticleTextBand(
|
|
int fallbackLeft,
|
|
int fallbackWidth,
|
|
const style::Markdown &st,
|
|
const LayoutContext &context) {
|
|
return context.useArticleBands
|
|
? PaddedBand(
|
|
context.articleLeft,
|
|
context.articleWidth,
|
|
st.textPadding)
|
|
: QRect(fallbackLeft, 0, std::max(fallbackWidth, 1), 0);
|
|
}
|
|
|
|
[[nodiscard]] int LimitedMediaWidth(
|
|
int availableWidth,
|
|
int intrinsicWidth) {
|
|
const auto scaledIntrinsic = style::ConvertScale(intrinsicWidth);
|
|
const auto limit = (scaledIntrinsic > 0)
|
|
? (2 * scaledIntrinsic)
|
|
: availableWidth;
|
|
return std::clamp(limit, 1, std::max(availableWidth, 1));
|
|
}
|
|
|
|
void ApplyMediaBlockGeometry(
|
|
LaidOutBlock *block,
|
|
QRect geometry,
|
|
const style::Markdown &st,
|
|
double mediaPixelScale) {
|
|
if (!block->mediaBlock) {
|
|
return;
|
|
}
|
|
block->mediaBlock->setMediaPixelScale(mediaPixelScale);
|
|
block->mediaBlock->setGeometry(geometry);
|
|
auto actual = block->mediaBlock->geometry();
|
|
if (actual.width() < geometry.width() && actual.x() == geometry.x()) {
|
|
geometry.moveLeft(
|
|
geometry.x() + std::max((geometry.width() - actual.width()) / 2, 0));
|
|
block->mediaBlock->setGeometry(geometry);
|
|
actual = block->mediaBlock->geometry();
|
|
}
|
|
block->mediaRect = actual;
|
|
|
|
// Media blocks without text report their top as the baseline, while
|
|
// the consumers (like list item marker placement) expect a text line
|
|
// baseline. Treat the media top as the top of a normal text line, so
|
|
// that markers are placed as if a text line started at the media top.
|
|
block->firstLineBaseline = std::max(
|
|
block->mediaBlock->firstLineBaseline(),
|
|
TextLineBaseline(st.body, block->mediaRect.y()));
|
|
|
|
block->visibleMediaRect = block->mediaRect;
|
|
}
|
|
|
|
void FillMediaCaption(
|
|
LaidOutBlock *block,
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
LayoutContext context) {
|
|
if (prepared.text.text.isEmpty() && !prepared.forceTextSegment) {
|
|
return;
|
|
}
|
|
block->supplementary = prepared.supplementary;
|
|
BuildOrReuseMarkedTextLeaf(
|
|
&block->leaf,
|
|
CachedTextLeafSlot::Leaf,
|
|
prepared,
|
|
st.body,
|
|
st,
|
|
prepared.text,
|
|
prepared.links,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
FlowTextMinResizeWidth(st.body),
|
|
context);
|
|
if (prepared.text.text.isEmpty() && !prepared.editPlaceholderText.isEmpty()) {
|
|
BuildOrReuseEditPlaceholderLeaf(
|
|
&block->placeholderText,
|
|
&block->placeholderLeaf,
|
|
prepared,
|
|
prepared.editPlaceholderText,
|
|
st.body,
|
|
PlainTextMinResizeWidth(st.body),
|
|
context);
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] LaidOutBlockLogicalGeometry ExtractLogicalGeometry(
|
|
const LaidOutBlock &block) {
|
|
return {
|
|
.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,
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] LaidOutBlock FinalizeLaidOutBlock(LaidOutBlock block) {
|
|
block.logicalGeometry = ExtractLogicalGeometry(block);
|
|
return block;
|
|
}
|
|
|
|
void ClearBlockGeometry(LaidOutBlock *block) {
|
|
if (!block) {
|
|
return;
|
|
}
|
|
block->outer = QRect();
|
|
block->headerRect = QRect();
|
|
block->bodyRect = QRect();
|
|
block->iconRect = QRect();
|
|
block->textRect = QRect();
|
|
block->labelRect = QRect();
|
|
block->subtitleRect = QRect();
|
|
block->actionRect = QRect();
|
|
block->markerRect = QRect();
|
|
block->contentRect = QRect();
|
|
block->formulaRect = QRect();
|
|
block->tableRect = QRect();
|
|
block->mediaRect = QRect();
|
|
block->thumbnailRect = QRect();
|
|
block->visibleFormulaRect = QRect();
|
|
block->scrollViewportRect = QRect();
|
|
block->scrollLogicalContentRect = QRect();
|
|
block->scrollScrollbarTrackRect = QRect();
|
|
block->scrollScrollbarThumbRect = QRect();
|
|
block->visibleTableRect = QRect();
|
|
block->tableScrollbarTrackRect = QRect();
|
|
block->tableScrollbarThumbRect = QRect();
|
|
block->visibleMediaRect = QRect();
|
|
block->markerCenter = QPoint();
|
|
block->logicalGeometry = {};
|
|
block->textWidth = 0;
|
|
block->labelWidth = 0;
|
|
block->subtitleWidth = 0;
|
|
block->actionWidth = 0;
|
|
block->markerWidth = 0;
|
|
block->firstLineBaseline = -1;
|
|
block->formulaAlign = style::al_left;
|
|
block->overflowed = false;
|
|
block->insideHorizontalScroll = false;
|
|
block->horizontalScrollMax = 0;
|
|
block->horizontalScrollAncestorShift = 0;
|
|
block->tableColumnWidths.clear();
|
|
}
|
|
|
|
void ResetTableCellGeometry(LaidOutTableCell *cell) {
|
|
if (!cell) {
|
|
return;
|
|
}
|
|
cell->logicalOuter = QRect();
|
|
cell->logicalTextRect = QRect();
|
|
cell->outer = QRect();
|
|
cell->textRect = QRect();
|
|
cell->textWidth = 0;
|
|
}
|
|
|
|
void ResetTableRowGeometry(LaidOutTableRow *row) {
|
|
if (!row) {
|
|
return;
|
|
}
|
|
row->logicalOuter = QRect();
|
|
row->outer = QRect();
|
|
for (auto &cell : row->cells) {
|
|
ResetTableCellGeometry(&cell);
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] bool TextNeedsRetainedLeaf(const QString &text) {
|
|
for (const auto ch : text) {
|
|
if (!Ui::Text::IsTrimmed(ch)
|
|
&& !Ui::Text::IsReplacedBySpace(ch)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
[[nodiscard]] bool MissingRetainedLeaf(
|
|
const QString &text,
|
|
const Ui::Text::String &leaf) {
|
|
return TextNeedsRetainedLeaf(text) && leaf.isEmpty();
|
|
}
|
|
|
|
[[nodiscard]] bool MissingRetainedPlaceholderLeaf(
|
|
bool usePlaceholder,
|
|
const Ui::Text::String &leaf) {
|
|
return usePlaceholder && leaf.isEmpty();
|
|
}
|
|
|
|
void FinishBlockGeometry(LaidOutBlock *block) {
|
|
if (!block) {
|
|
return;
|
|
}
|
|
block->logicalGeometry = ExtractLogicalGeometry(*block);
|
|
}
|
|
|
|
[[nodiscard]] int ScrollbarReserveHeight(
|
|
bool scrollOwner,
|
|
int horizontalScrollMax,
|
|
const style::Markdown &st) {
|
|
return (scrollOwner && (horizontalScrollMax > 0))
|
|
? (st.table.scrollbarSkip + st.table.scrollbarHeight)
|
|
: 0;
|
|
}
|
|
|
|
void CopyCachedTextLeaf(
|
|
CachedTextLeafPool *pool,
|
|
CachedTextLeafKey key,
|
|
CachedTextLeafSourceSignature source,
|
|
Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId syntaxHighlightProcessId = 0) {
|
|
if (!pool || !leaf || leaf->isEmpty()) {
|
|
return;
|
|
}
|
|
pool->entries.insert_or_assign(
|
|
std::move(key),
|
|
CachedTextLeafEntry{
|
|
.leaf = std::move(*leaf),
|
|
.source = std::move(source),
|
|
.syntaxHighlightProcessId = syntaxHighlightProcessId,
|
|
});
|
|
*leaf = Ui::Text::String();
|
|
}
|
|
|
|
void CopyBlockCachedTextLeafs(
|
|
const PreparedBlock &prepared,
|
|
LaidOutBlock &block,
|
|
const style::Markdown &st,
|
|
CachedTextLeafPool *pool,
|
|
const std::vector<int> &preparedPath) {
|
|
const auto copyBlockLeaf = [&](CachedTextLeafSlot slot,
|
|
CachedTextLeafSourceSignature source,
|
|
Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId syntaxHighlightProcessId = 0) {
|
|
CopyCachedTextLeaf(
|
|
pool,
|
|
BlockCachedTextLeafKey(slot, prepared, preparedPath),
|
|
std::move(source),
|
|
leaf,
|
|
syntaxHighlightProcessId);
|
|
};
|
|
const auto copyTableCellLeaf = [&](
|
|
CachedTextLeafSlot slot,
|
|
const PreparedTableCell &preparedCell,
|
|
int tableRowIndex,
|
|
int tableCellIndex,
|
|
CachedTextLeafSourceSignature source,
|
|
Ui::Text::String *leaf) {
|
|
CopyCachedTextLeaf(
|
|
pool,
|
|
TableCellCachedTextLeafKey(
|
|
slot,
|
|
preparedCell,
|
|
preparedPath,
|
|
tableRowIndex,
|
|
tableCellIndex),
|
|
std::move(source),
|
|
leaf);
|
|
};
|
|
|
|
if (!block.marker.isEmpty()) {
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Marker,
|
|
PlainTextLeafSourceSignature(
|
|
ListMarkerText(prepared),
|
|
st.body,
|
|
PlainTextMinResizeWidth(st.body)),
|
|
&block.marker);
|
|
}
|
|
|
|
switch (prepared.kind) {
|
|
case PreparedBlockKind::Paragraph:
|
|
case PreparedBlockKind::Thinking:
|
|
case PreparedBlockKind::Heading: {
|
|
const auto &textStyle = TextStyleFor(prepared, st);
|
|
const auto &placeholderStyle = EditPlaceholderTextStyleFor(
|
|
prepared,
|
|
st);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Leaf,
|
|
MarkedTextLeafSourceSignature(
|
|
prepared.text,
|
|
textStyle,
|
|
FlowBlockMinimumWidth(prepared, st)),
|
|
&block.leaf);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Placeholder,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.editPlaceholderText,
|
|
placeholderStyle,
|
|
PlainTextMinResizeWidth(placeholderStyle)),
|
|
&block.placeholderLeaf);
|
|
} break;
|
|
case PreparedBlockKind::CodeBlock:
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Leaf,
|
|
CodeTextLeafSourceSignature(prepared, st),
|
|
&block.leaf,
|
|
block.syntaxHighlightProcessId);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Placeholder,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.editPlaceholderText,
|
|
st.code,
|
|
PlainTextMinResizeWidth(st.code)),
|
|
&block.placeholderLeaf);
|
|
break;
|
|
case PreparedBlockKind::DisplayMath:
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Placeholder,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.editPlaceholderText,
|
|
st.displayMath.fallbackStyle,
|
|
DisplayMathFallbackTextMinResizeWidth(st)),
|
|
&block.placeholderLeaf);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Fallback,
|
|
MarkedTextLeafSourceSignature(
|
|
DisplayMathFallbackText(),
|
|
st.displayMath.fallbackStyle,
|
|
DisplayMathFallbackTextMinResizeWidth(st)),
|
|
&block.fallbackLeaf);
|
|
break;
|
|
case PreparedBlockKind::Table: {
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Leaf,
|
|
MarkedTextLeafSourceSignature(
|
|
prepared.text,
|
|
st.body,
|
|
FlowTextMinResizeWidth(st.body)),
|
|
&block.leaf);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Placeholder,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.editPlaceholderText,
|
|
st.body,
|
|
PlainTextMinResizeWidth(st.body)),
|
|
&block.placeholderLeaf);
|
|
const auto rowCount = std::min(
|
|
int(prepared.tableRows.size()),
|
|
int(block.tableRows.size()));
|
|
for (auto rowIndex = 0; rowIndex != rowCount; ++rowIndex) {
|
|
const auto cellCount = std::min(
|
|
int(prepared.tableRows[rowIndex].cells.size()),
|
|
int(block.tableRows[rowIndex].cells.size()));
|
|
for (auto cellIndex = 0; cellIndex != cellCount; ++cellIndex) {
|
|
const auto &preparedCell = prepared.tableRows[rowIndex].cells[cellIndex];
|
|
auto &cell = block.tableRows[rowIndex].cells[cellIndex];
|
|
const auto &textStyle = preparedCell.header
|
|
? st.table.headerStyle
|
|
: st.table.bodyStyle;
|
|
const auto minResizeWidth = TableCellTextMinResizeWidth(
|
|
textStyle,
|
|
st);
|
|
copyTableCellLeaf(
|
|
CachedTextLeafSlot::TableCellText,
|
|
preparedCell,
|
|
rowIndex,
|
|
cellIndex,
|
|
MarkedTextLeafSourceSignature(
|
|
preparedCell.text,
|
|
textStyle,
|
|
minResizeWidth),
|
|
&cell.leaf);
|
|
copyTableCellLeaf(
|
|
CachedTextLeafSlot::TableCellPlaceholder,
|
|
preparedCell,
|
|
rowIndex,
|
|
cellIndex,
|
|
PlainTextLeafSourceSignature(
|
|
preparedCell.editPlaceholderText,
|
|
textStyle,
|
|
minResizeWidth),
|
|
&cell.placeholderLeaf);
|
|
}
|
|
}
|
|
} break;
|
|
case PreparedBlockKind::Details:
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Leaf,
|
|
MarkedTextLeafSourceSignature(
|
|
prepared.text,
|
|
st.details.summaryStyle,
|
|
FlowTextMinResizeWidth(st.details.summaryStyle)),
|
|
&block.leaf);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Placeholder,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.editPlaceholderText,
|
|
st.details.summaryStyle,
|
|
PlainTextMinResizeWidth(st.details.summaryStyle)),
|
|
&block.placeholderLeaf);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Action,
|
|
PlainTextLeafSourceSignature(
|
|
DetailsStateText(prepared.detailsOpen),
|
|
st.details.summaryStyle,
|
|
PlainTextMinResizeWidth(st.details.summaryStyle)),
|
|
&block.actionLeaf);
|
|
break;
|
|
case PreparedBlockKind::Placeholder:
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Label,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.placeholder.label,
|
|
st.placeholder.labelStyle,
|
|
PlainTextMinResizeWidth(st.placeholder.labelStyle)),
|
|
&block.labelLeaf);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Leaf,
|
|
MarkedTextLeafSourceSignature(
|
|
prepared.text,
|
|
st.body,
|
|
FlowTextMinResizeWidth(st.body)),
|
|
&block.leaf);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Placeholder,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.editPlaceholderText,
|
|
st.body,
|
|
PlainTextMinResizeWidth(st.body)),
|
|
&block.placeholderLeaf);
|
|
break;
|
|
case PreparedBlockKind::RelatedArticle:
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Label,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.relatedArticle.title,
|
|
st.relatedArticle.titleStyle,
|
|
PlainTextMinResizeWidth(st.relatedArticle.titleStyle)),
|
|
&block.labelLeaf);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Subtitle,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.relatedArticle.description,
|
|
st.relatedArticle.subtitleStyle,
|
|
PlainTextMinResizeWidth(st.relatedArticle.subtitleStyle)),
|
|
&block.subtitleLeaf);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Action,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.relatedArticle.footer,
|
|
st.relatedArticle.footerStyle,
|
|
PlainTextMinResizeWidth(st.relatedArticle.footerStyle)),
|
|
&block.actionLeaf);
|
|
break;
|
|
case PreparedBlockKind::EmbedPost:
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Label,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.embedPost.author,
|
|
st.embedPost.authorStyle,
|
|
PlainTextMinResizeWidth(st.embedPost.authorStyle)),
|
|
&block.labelLeaf);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Subtitle,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.embedPost.dateText,
|
|
st.embedPost.dateStyle,
|
|
PlainTextMinResizeWidth(st.embedPost.dateStyle)),
|
|
&block.subtitleLeaf);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Leaf,
|
|
MarkedTextLeafSourceSignature(
|
|
prepared.text,
|
|
st.body,
|
|
FlowTextMinResizeWidth(st.body)),
|
|
&block.leaf);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Placeholder,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.editPlaceholderText,
|
|
st.body,
|
|
PlainTextMinResizeWidth(st.body)),
|
|
&block.placeholderLeaf);
|
|
break;
|
|
case PreparedBlockKind::Photo:
|
|
case PreparedBlockKind::Video:
|
|
case PreparedBlockKind::Audio:
|
|
case PreparedBlockKind::Map:
|
|
case PreparedBlockKind::Channel:
|
|
case PreparedBlockKind::GroupedMedia:
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Leaf,
|
|
MarkedTextLeafSourceSignature(
|
|
prepared.text,
|
|
st.body,
|
|
FlowTextMinResizeWidth(st.body)),
|
|
&block.leaf);
|
|
copyBlockLeaf(
|
|
CachedTextLeafSlot::Placeholder,
|
|
PlainTextLeafSourceSignature(
|
|
prepared.editPlaceholderText,
|
|
st.body,
|
|
PlainTextMinResizeWidth(st.body)),
|
|
&block.placeholderLeaf);
|
|
break;
|
|
case PreparedBlockKind::Rule:
|
|
case PreparedBlockKind::Quote:
|
|
case PreparedBlockKind::List:
|
|
case PreparedBlockKind::ListItem:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CopyBlockCachedTextLeafs(
|
|
const std::vector<PreparedBlock> &preparedBlocks,
|
|
std::vector<LaidOutBlock> *blocks,
|
|
const style::Markdown &st,
|
|
CachedTextLeafPool *pool,
|
|
std::vector<int> *preparedPath) {
|
|
if (!pool || !blocks || !preparedPath) {
|
|
return;
|
|
}
|
|
const auto count = std::min(int(preparedBlocks.size()), int(blocks->size()));
|
|
for (auto i = 0; i != count; ++i) {
|
|
preparedPath->push_back(i);
|
|
CopyBlockCachedTextLeafs(
|
|
preparedBlocks[i],
|
|
(*blocks)[i],
|
|
st,
|
|
pool,
|
|
*preparedPath);
|
|
preparedPath->pop_back();
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void BuildOrReuseMarkedTextLeaf(
|
|
Ui::Text::String *leaf,
|
|
CachedTextLeafSlot slot,
|
|
const PreparedBlock &prepared,
|
|
const style::TextStyle &textStyle,
|
|
const style::Markdown &st,
|
|
const TextWithEntities &text,
|
|
const std::vector<PreparedLink> &links,
|
|
const std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
int minResizeWidth,
|
|
LayoutContext context) {
|
|
BuildOrReuseCachedTextLeaf(
|
|
leaf,
|
|
nullptr,
|
|
context,
|
|
BlockCachedTextLeafKey(slot, prepared, context.preparedPath),
|
|
MarkedTextLeafSourceSignature(text, textStyle, minResizeWidth),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetTextLeaf(
|
|
leaf,
|
|
textStyle,
|
|
st,
|
|
text,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
minResizeWidth,
|
|
context.repaint,
|
|
context.repaintRect);
|
|
BindLinks(leaf, links);
|
|
});
|
|
BindLinks(leaf, links);
|
|
}
|
|
|
|
void BuildOrReusePlainTextLeaf(
|
|
Ui::Text::String *leaf,
|
|
CachedTextLeafSlot slot,
|
|
const PreparedBlock &prepared,
|
|
const style::TextStyle &textStyle,
|
|
const QString &text,
|
|
int minResizeWidth,
|
|
LayoutContext context) {
|
|
BuildOrReuseCachedTextLeaf(
|
|
leaf,
|
|
nullptr,
|
|
context,
|
|
BlockCachedTextLeafKey(slot, prepared, context.preparedPath),
|
|
PlainTextLeafSourceSignature(text, textStyle, minResizeWidth),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetPlainTextLeaf(leaf, textStyle, text, minResizeWidth);
|
|
});
|
|
}
|
|
|
|
void BuildOrReuseEditPlaceholderLeaf(
|
|
QString *placeholderText,
|
|
Ui::Text::String *placeholderLeaf,
|
|
const PreparedBlock &prepared,
|
|
const QString &text,
|
|
const style::TextStyle &textStyle,
|
|
int minResizeWidth,
|
|
LayoutContext context) {
|
|
if (text.isEmpty()) {
|
|
return;
|
|
}
|
|
*placeholderText = text;
|
|
BuildOrReusePlainTextLeaf(
|
|
placeholderLeaf,
|
|
CachedTextLeafSlot::Placeholder,
|
|
prepared,
|
|
textStyle,
|
|
*placeholderText,
|
|
minResizeWidth,
|
|
context);
|
|
}
|
|
|
|
void CopyCachedTextLeafs(
|
|
const std::vector<PreparedBlock> &preparedBlocks,
|
|
std::vector<LaidOutBlock> *blocks,
|
|
const style::Markdown &st,
|
|
CachedTextLeafPool *pool) {
|
|
if (!pool) {
|
|
return;
|
|
}
|
|
pool->entries.clear();
|
|
auto preparedPath = std::vector<int>();
|
|
CopyBlockCachedTextLeafs(
|
|
preparedBlocks,
|
|
blocks,
|
|
st,
|
|
pool,
|
|
&preparedPath);
|
|
}
|
|
|
|
size_t CachedTextLeafKeyHasher::operator()(
|
|
const CachedTextLeafKey &value) const noexcept {
|
|
auto result = CombineHash(0, size_t(value.slot));
|
|
result = CombineHash(result, size_t(value.identityKind));
|
|
switch (value.identityKind) {
|
|
case CachedTextLeafIdentityKind::PreparedPath:
|
|
result = CombineHash(result, size_t(value.preparedPath.size() + 1));
|
|
for (const auto step : value.preparedPath) {
|
|
result = CombineHash(result, size_t(step + 1));
|
|
}
|
|
result = CombineHash(result, size_t(value.tableRowIndex + 1));
|
|
return CombineHash(result, size_t(value.tableCellIndex + 1));
|
|
case CachedTextLeafIdentityKind::EditBlock:
|
|
return CombineHash(result, HashPreparedEditBlockSource(value.editBlock));
|
|
case CachedTextLeafIdentityKind::EditListItem:
|
|
return CombineHash(
|
|
result,
|
|
HashPreparedEditListItemSource(value.editListItem));
|
|
case CachedTextLeafIdentityKind::EditTableCell:
|
|
return CombineHash(
|
|
result,
|
|
HashPreparedEditTableCellSource(value.editTableCell));
|
|
case CachedTextLeafIdentityKind::EditLeaf:
|
|
return CombineHash(result, HashPreparedEditLeafSource(value.editLeaf));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
LayoutContextScope::LayoutContextScope(const LayoutContext &context)
|
|
: _previous(CurrentLayoutContext) {
|
|
CurrentLayoutContext = &context;
|
|
}
|
|
|
|
LayoutContextScope::~LayoutContextScope() {
|
|
CurrentLayoutContext = _previous;
|
|
}
|
|
|
|
void FillMediaCaptionContent(
|
|
LaidOutBlock *block,
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
LayoutContext context) {
|
|
FillMediaCaption(
|
|
block,
|
|
prepared,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
context);
|
|
}
|
|
|
|
void LayoutMediaCaption(
|
|
LaidOutBlock *block,
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int skip,
|
|
int *bottom,
|
|
LayoutContext context) {
|
|
FillMediaCaption(
|
|
block,
|
|
prepared,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
context);
|
|
const auto counted = LayoutMediaCaptionGeometry(
|
|
block,
|
|
prepared,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
skip,
|
|
bottom,
|
|
context);
|
|
Expects(counted);
|
|
}
|
|
|
|
bool LayoutMediaCaptionGeometry(
|
|
LaidOutBlock *block,
|
|
const PreparedBlock &prepared,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int skip,
|
|
int *bottom,
|
|
LayoutContext context) {
|
|
if (!block || !bottom) {
|
|
return false;
|
|
}
|
|
if (prepared.text.text.isEmpty() && !prepared.forceTextSegment) {
|
|
return true;
|
|
}
|
|
if (MissingRetainedLeaf(prepared.text.text, block->leaf)) {
|
|
return false;
|
|
}
|
|
block->supplementary = prepared.supplementary;
|
|
const auto textBand = ArticleTextBand(left, width, st, context);
|
|
block->textWidth = std::max(textBand.width(), 1);
|
|
block->textRect = QRect(
|
|
textBand.x(),
|
|
top + skip,
|
|
block->textWidth,
|
|
ResolveEditableHeight(
|
|
std::max(
|
|
block->leaf.countHeight(block->textWidth, true),
|
|
TextLineHeight(st.body)),
|
|
context));
|
|
*bottom = block->textRect.y() + block->textRect.height();
|
|
return true;
|
|
}
|
|
|
|
[[nodiscard]] int SingleDigitOrderedMarkerWidth(
|
|
const style::Markdown &st) {
|
|
return std::max(
|
|
st.body.font->width(u"8."_q),
|
|
st.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;
|
|
}
|
|
|
|
TextWithEntities CodeBlockDisplayText(TextWithEntities text) {
|
|
auto result = TextWithEntities();
|
|
const auto sourceLength = int(text.text.size());
|
|
result.text.reserve(sourceLength);
|
|
result.entities.reserve(text.entities.size());
|
|
|
|
auto offsets = std::vector<int>();
|
|
offsets.reserve(sourceLength + 1);
|
|
auto column = 0;
|
|
for (auto i = 0; i != sourceLength; ++i) {
|
|
offsets.push_back(result.text.size());
|
|
const auto ch = text.text[i];
|
|
if (ch == QChar::Tabulation) {
|
|
const auto count = kCodeTabColumns - (column % kCodeTabColumns);
|
|
for (auto j = 0; j != count; ++j) {
|
|
result.text.append(QChar::Space);
|
|
}
|
|
column += count;
|
|
continue;
|
|
}
|
|
result.text.append(ch);
|
|
if (Ui::Text::IsNewline(ch)) {
|
|
column = 0;
|
|
} else {
|
|
++column;
|
|
}
|
|
}
|
|
offsets.push_back(result.text.size());
|
|
|
|
for (const auto &entity : text.entities) {
|
|
const auto from = entity.offset();
|
|
const auto length = entity.length();
|
|
if (entity.type() == EntityType::Pre
|
|
|| from < 0
|
|
|| from >= sourceLength
|
|
|| length <= 0) {
|
|
continue;
|
|
}
|
|
const auto till = from + std::min(length, sourceLength - from);
|
|
if (till <= from) {
|
|
continue;
|
|
}
|
|
const auto displayFrom = offsets[from];
|
|
const auto displayTill = offsets[till];
|
|
if (displayTill <= displayFrom) {
|
|
continue;
|
|
}
|
|
result.entities.push_back(EntityInText(
|
|
entity.type(),
|
|
displayFrom,
|
|
displayTill - displayFrom,
|
|
entity.data()));
|
|
}
|
|
if (result.text.isEmpty() || Ui::Text::IsTrimmed(result.text.back())) {
|
|
result.text.append(QChar(kCodeTrailingGuard));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool IsFlowKind(PreparedBlockKind kind) {
|
|
return (kind == PreparedBlockKind::Paragraph)
|
|
|| (kind == PreparedBlockKind::Thinking)
|
|
|| (kind == PreparedBlockKind::Heading);
|
|
}
|
|
|
|
bool IsAnchorOnlyBlock(const PreparedBlock &block) {
|
|
return (block.kind == PreparedBlockKind::Paragraph)
|
|
&& !block.anchorId.isEmpty()
|
|
&& !TextNeedsRetainedLeaf(block.text.text)
|
|
&& block.text.entities.empty()
|
|
&& block.links.empty()
|
|
&& block.children.empty();
|
|
}
|
|
|
|
QString ListMarkerText(const PreparedBlock &block) {
|
|
if (block.listKind == ListKind::Ordered) {
|
|
if (!block.orderedMarkerText.isEmpty()) {
|
|
return FormatPreparedOrderedRawMarkerText(
|
|
block.orderedMarkerText,
|
|
block.listDelimiter);
|
|
}
|
|
if (!block.articleOrderedMarkerText.isEmpty()) {
|
|
return FormatPreparedOrderedRawMarkerText(
|
|
block.articleOrderedMarkerText,
|
|
block.listDelimiter);
|
|
}
|
|
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);
|
|
}
|
|
|
|
int TextLineAscent(const style::TextStyle &style) {
|
|
if (style.qtextEditLineMetrics) {
|
|
const auto lineHeight = QFixed(TextLineHeight(style));
|
|
const auto leading = std::max(style.font->fleading, QFixed());
|
|
return std::clamp(
|
|
(lineHeight * 4 / 5) - leading,
|
|
QFixed(),
|
|
lineHeight).toInt();
|
|
}
|
|
const auto lineHeight = TextLineHeight(style);
|
|
const auto textTop = std::max(lineHeight - style.font->height, 0) / 2;
|
|
return textTop + style.font->ascent;
|
|
}
|
|
|
|
int TextLineBaseline(
|
|
const style::TextStyle &style,
|
|
int top) {
|
|
return top + TextLineAscent(style);
|
|
}
|
|
|
|
int ResolveEditableHeight(
|
|
int naturalHeight,
|
|
LayoutContext context) {
|
|
const auto state = context.editableHeightOverride;
|
|
if (!state) {
|
|
return naturalHeight;
|
|
}
|
|
const auto index = state->nextEditableIndex++;
|
|
return (index == state->editableIndex)
|
|
? std::max(state->height, 1)
|
|
: naturalHeight;
|
|
}
|
|
|
|
[[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()
|
|
? TextLineBaseline(style)
|
|
: lines.front().baseline);
|
|
}
|
|
|
|
QPoint BulletMarkerCenter(
|
|
int left,
|
|
int baseline,
|
|
const style::Markdown &st) {
|
|
const auto &list = st.list;
|
|
const auto lineHeight = TextLineHeight(st.body);
|
|
const auto markerWidth = SingleDigitOrderedMarkerWidth(st);
|
|
const auto nominalBaseline = TextLineBaseline(st.body);
|
|
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);
|
|
}
|
|
|
|
int HorizontalMarginsWidth(QMargins margins) {
|
|
return std::max(margins.left(), 0) + std::max(margins.right(), 0);
|
|
}
|
|
|
|
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 ReadableTextMinWidth(const style::TextStyle &style) {
|
|
return std::max({
|
|
style.font->spacew * kReadableTextColumns,
|
|
TextLineHeight(style) * kReadableTextLineHeights,
|
|
1,
|
|
});
|
|
}
|
|
|
|
int FlowTextMinResizeWidth(const style::TextStyle &style) {
|
|
return ReadableTextMinWidth(style);
|
|
}
|
|
|
|
int LeafMinimumWidth(const Ui::Text::String &leaf) {
|
|
return leaf.isEmpty()
|
|
? 0
|
|
: std::min(leaf.minResizeWidth(), leaf.maxWidth());
|
|
}
|
|
|
|
int FlowBlockMinimumWidth(
|
|
const PreparedBlock &prepared,
|
|
const style::Markdown &st) {
|
|
if (IsAnchorOnlyBlock(prepared)
|
|
|| (prepared.text.text.isEmpty() && !prepared.forceTextSegment)) {
|
|
return 1;
|
|
}
|
|
return ReadableTextMinWidth(TextStyleFor(prepared, st));
|
|
}
|
|
|
|
int FlowBlockPreferredWidth(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> &formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
LayoutContext context) {
|
|
if (IsAnchorOnlyBlock(prepared)) {
|
|
return 1;
|
|
}
|
|
const auto &textStyle = TextStyleFor(prepared, st);
|
|
const auto minResizeWidth = FlowBlockMinimumWidth(prepared, st);
|
|
const auto usePlaceholder = prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty();
|
|
if (!usePlaceholder) {
|
|
return WithCachedTextLeaf(
|
|
context,
|
|
BlockCachedTextLeafKey(
|
|
CachedTextLeafSlot::Leaf,
|
|
prepared,
|
|
context.preparedPath),
|
|
MarkedTextLeafSourceSignature(
|
|
prepared.text,
|
|
textStyle,
|
|
minResizeWidth),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetTextLeaf(
|
|
leaf,
|
|
textStyle,
|
|
st,
|
|
prepared.text,
|
|
&formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
minResizeWidth,
|
|
context.repaint,
|
|
context.repaintRect);
|
|
BindLinks(leaf, prepared.links);
|
|
},
|
|
[](const Ui::Text::String &leaf,
|
|
Spellchecker::HighlightProcessId) {
|
|
return leaf.maxWidth();
|
|
});
|
|
}
|
|
const auto &placeholderStyle = EditPlaceholderTextStyleFor(prepared, st);
|
|
return WithCachedTextLeaf(
|
|
context,
|
|
BlockCachedTextLeafKey(
|
|
CachedTextLeafSlot::Placeholder,
|
|
prepared,
|
|
context.preparedPath),
|
|
PlainTextLeafSourceSignature(
|
|
prepared.editPlaceholderText,
|
|
placeholderStyle,
|
|
PlainTextMinResizeWidth(placeholderStyle)),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetPlainTextLeaf(
|
|
leaf,
|
|
placeholderStyle,
|
|
prepared.editPlaceholderText,
|
|
PlainTextMinResizeWidth(placeholderStyle));
|
|
},
|
|
[](const Ui::Text::String &leaf,
|
|
Spellchecker::HighlightProcessId) {
|
|
return leaf.maxWidth();
|
|
});
|
|
}
|
|
|
|
int FlowBlockContentMinimumWidth(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> &formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
LayoutContext context) {
|
|
const auto base = FlowBlockMinimumWidth(prepared, st);
|
|
if (IsAnchorOnlyBlock(prepared) || prepared.text.text.isEmpty()) {
|
|
return base;
|
|
}
|
|
const auto &textStyle = TextStyleFor(prepared, st);
|
|
const auto minResizeWidth = FlowBlockMinimumWidth(prepared, st);
|
|
return WithCachedTextLeaf(
|
|
context,
|
|
BlockCachedTextLeafKey(
|
|
CachedTextLeafSlot::Leaf,
|
|
prepared,
|
|
context.preparedPath),
|
|
MarkedTextLeafSourceSignature(
|
|
prepared.text,
|
|
textStyle,
|
|
minResizeWidth),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetTextLeaf(
|
|
leaf,
|
|
textStyle,
|
|
st,
|
|
prepared.text,
|
|
&formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
minResizeWidth,
|
|
context.repaint,
|
|
context.repaintRect);
|
|
BindLinks(leaf, prepared.links);
|
|
},
|
|
[&](const Ui::Text::String &leaf,
|
|
Spellchecker::HighlightProcessId) {
|
|
return std::max(base, LeafMinimumWidth(leaf));
|
|
});
|
|
}
|
|
|
|
int DetailsSummaryContentMinimumWidth(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> &formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
LayoutContext context) {
|
|
const auto base = ReadableTextMinWidth(st.details.summaryStyle);
|
|
if (prepared.text.text.isEmpty()) {
|
|
return base;
|
|
}
|
|
const auto minResizeWidth = FlowTextMinResizeWidth(
|
|
st.details.summaryStyle);
|
|
return WithCachedTextLeaf(
|
|
context,
|
|
BlockCachedTextLeafKey(
|
|
CachedTextLeafSlot::Leaf,
|
|
prepared,
|
|
context.preparedPath),
|
|
MarkedTextLeafSourceSignature(
|
|
prepared.text,
|
|
st.details.summaryStyle,
|
|
minResizeWidth),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetTextLeaf(
|
|
leaf,
|
|
st.details.summaryStyle,
|
|
st,
|
|
prepared.text,
|
|
&formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
minResizeWidth,
|
|
context.repaint,
|
|
context.repaintRect);
|
|
BindLinks(leaf, prepared.links);
|
|
},
|
|
[&](const Ui::Text::String &leaf,
|
|
Spellchecker::HighlightProcessId) {
|
|
return std::max(base, LeafMinimumWidth(leaf));
|
|
});
|
|
}
|
|
|
|
int CodeBlockMinimumWidth(const style::Markdown &st) {
|
|
const auto padding = BlockquotePadding(st.code.pre);
|
|
const auto content = std::max({
|
|
st.code.font->spacew * kReadableCodeColumns,
|
|
ReadableTextMinWidth(st.code),
|
|
1,
|
|
});
|
|
return HorizontalMarginsWidth(padding) + content;
|
|
}
|
|
|
|
int CodeTextMinResizeWidth(const style::Markdown &st) {
|
|
return std::max(
|
|
CodeBlockMinimumWidth(st)
|
|
- HorizontalMarginsWidth(BlockquotePadding(st.code.pre)),
|
|
1);
|
|
}
|
|
|
|
int CodeBlockPreferredWidth(
|
|
const PreparedBlock &prepared,
|
|
const style::Markdown &st,
|
|
LayoutContext context) {
|
|
const auto padding = BlockquotePadding(st.code.pre);
|
|
const auto usePlaceholder = prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty();
|
|
if (usePlaceholder) {
|
|
return HorizontalMarginsWidth(padding) + WithCachedTextLeaf(
|
|
context,
|
|
BlockCachedTextLeafKey(
|
|
CachedTextLeafSlot::Placeholder,
|
|
prepared,
|
|
context.preparedPath),
|
|
PlainTextLeafSourceSignature(
|
|
prepared.editPlaceholderText,
|
|
st.code,
|
|
PlainTextMinResizeWidth(st.code)),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetPlainTextLeaf(
|
|
leaf,
|
|
st.code,
|
|
prepared.editPlaceholderText,
|
|
PlainTextMinResizeWidth(st.code));
|
|
},
|
|
[](const Ui::Text::String &leaf,
|
|
Spellchecker::HighlightProcessId) {
|
|
return leaf.maxWidth();
|
|
});
|
|
}
|
|
return HorizontalMarginsWidth(padding) + WithCachedTextLeaf(
|
|
context,
|
|
BlockCachedTextLeafKey(
|
|
CachedTextLeafSlot::Leaf,
|
|
prepared,
|
|
context.preparedPath),
|
|
CodeTextLeafSourceSignature(prepared, st),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId *syntaxHighlightProcessId) {
|
|
PopulateCodeBlockLeaf(
|
|
leaf,
|
|
syntaxHighlightProcessId,
|
|
prepared.text,
|
|
prepared.links,
|
|
prepared.codeLanguage,
|
|
nullptr,
|
|
nullptr,
|
|
nullptr,
|
|
st,
|
|
context.allowAsyncSyntaxHighlighting,
|
|
context.syntaxHighlightTracker,
|
|
context.repaint,
|
|
context.repaintRect,
|
|
context.spoilerLinkFilter);
|
|
},
|
|
[](const Ui::Text::String &leaf,
|
|
Spellchecker::HighlightProcessId) {
|
|
return leaf.maxWidth();
|
|
});
|
|
}
|
|
|
|
int DisplayMathMinimumWidth(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> &formulas,
|
|
const style::Markdown &st) {
|
|
const auto &padding = st.displayMath.padding;
|
|
if (const auto formula = PreparedFormulaFor(formulas, prepared.formulaIndex);
|
|
formula && formula->measured.success) {
|
|
return HorizontalMarginsWidth(padding)
|
|
+ std::max(formula->measured.logicalSize.width(), 1);
|
|
}
|
|
const auto &fallbackPadding = st.displayMath.fallbackPadding;
|
|
auto fallbackText = DisplayMathFallbackText();
|
|
auto leaf = Ui::Text::String(ReadableTextMinWidth(st.displayMath.fallbackStyle));
|
|
leaf.setMarkedText(
|
|
st.displayMath.fallbackStyle,
|
|
std::move(fallbackText),
|
|
kIvMarkedTextOptions);
|
|
return HorizontalMarginsWidth(padding)
|
|
+ HorizontalMarginsWidth(fallbackPadding)
|
|
+ std::max({
|
|
leaf.maxWidth(),
|
|
ReadableTextMinWidth(st.displayMath.fallbackStyle),
|
|
1,
|
|
});
|
|
}
|
|
|
|
int DisplayMathPreferredWidth(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> &formulas,
|
|
const style::Markdown &st,
|
|
LayoutContext context) {
|
|
const auto &padding = st.displayMath.padding;
|
|
if (const auto formula = PreparedFormulaFor(formulas, prepared.formulaIndex);
|
|
formula && formula->measured.success) {
|
|
return HorizontalMarginsWidth(padding)
|
|
+ std::max(formula->measured.logicalSize.width(), 1);
|
|
}
|
|
const auto &fallbackPadding = st.displayMath.fallbackPadding;
|
|
const auto usePlaceholder = prepared.formulaTex.trimmed().isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty();
|
|
if (usePlaceholder) {
|
|
return HorizontalMarginsWidth(padding)
|
|
+ HorizontalMarginsWidth(fallbackPadding)
|
|
+ WithCachedTextLeaf(
|
|
context,
|
|
BlockCachedTextLeafKey(
|
|
CachedTextLeafSlot::Placeholder,
|
|
prepared,
|
|
context.preparedPath),
|
|
PlainTextLeafSourceSignature(
|
|
prepared.editPlaceholderText,
|
|
st.displayMath.fallbackStyle,
|
|
DisplayMathFallbackTextMinResizeWidth(st)),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetPlainTextLeaf(
|
|
leaf,
|
|
st.displayMath.fallbackStyle,
|
|
prepared.editPlaceholderText,
|
|
DisplayMathFallbackTextMinResizeWidth(st));
|
|
},
|
|
[](const Ui::Text::String &leaf,
|
|
Spellchecker::HighlightProcessId) {
|
|
return leaf.maxWidth();
|
|
});
|
|
}
|
|
return HorizontalMarginsWidth(padding)
|
|
+ HorizontalMarginsWidth(fallbackPadding)
|
|
+ WithCachedTextLeaf(
|
|
context,
|
|
BlockCachedTextLeafKey(
|
|
CachedTextLeafSlot::Fallback,
|
|
prepared,
|
|
context.preparedPath),
|
|
MarkedTextLeafSourceSignature(
|
|
DisplayMathFallbackText(),
|
|
st.displayMath.fallbackStyle,
|
|
DisplayMathFallbackTextMinResizeWidth(st)),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
*leaf = Ui::Text::String(
|
|
TextMinResizeWidth(
|
|
DisplayMathFallbackTextMinResizeWidth(st)));
|
|
leaf->setMarkedText(
|
|
st.displayMath.fallbackStyle,
|
|
DisplayMathFallbackText(),
|
|
kIvMarkedTextOptions);
|
|
},
|
|
[](const Ui::Text::String &leaf,
|
|
Spellchecker::HighlightProcessId) {
|
|
return leaf.maxWidth();
|
|
});
|
|
}
|
|
|
|
int PlainTextMinResizeWidth(const style::TextStyle &style) {
|
|
return std::max(style.font->spacew, 1);
|
|
}
|
|
|
|
int DisplayMathFallbackTextMinResizeWidth(const style::Markdown &st) {
|
|
return PlainTextMinResizeWidth(st.displayMath.fallbackStyle);
|
|
}
|
|
|
|
int ContainerMinimumWidth(
|
|
int contentMinimumWidth,
|
|
QMargins padding) {
|
|
return std::max(contentMinimumWidth, 1) + HorizontalMarginsWidth(padding);
|
|
}
|
|
|
|
int TableMinimumGridWidth(
|
|
int columnCount,
|
|
const style::Markdown &st,
|
|
bool bordered) {
|
|
if (columnCount <= 0) {
|
|
return 0;
|
|
}
|
|
const auto border = TableBorder(bordered, st);
|
|
const auto minimum = st.table.minColumnWidth;
|
|
return border + columnCount * (minimum + border);
|
|
}
|
|
|
|
int TableCellTextMinResizeWidth(
|
|
const style::TextStyle &textStyle,
|
|
const style::Markdown &st) {
|
|
const auto &padding = st.table.cellPadding;
|
|
return std::max({
|
|
st.table.minColumnWidth - padding.left() - padding.right(),
|
|
textStyle.font->spacew,
|
|
1,
|
|
});
|
|
}
|
|
|
|
std::vector<int> ComputeTableColumnMinimumWidths(
|
|
std::vector<TableCellMinimumWidthConstraint> constraints,
|
|
int columnCount,
|
|
const style::Markdown &st,
|
|
bool bordered) {
|
|
// Fully empty columns may shrink below st.table.minColumnWidth,
|
|
// to the cell padding and some non-zero content width.
|
|
const auto &padding = st.table.cellPadding;
|
|
const auto emptyMinimum = padding.left()
|
|
+ padding.right()
|
|
+ std::max(st.table.bodyStyle.font->spacew, 1);
|
|
auto result = std::vector<int>(std::max(columnCount, 0), emptyMinimum);
|
|
if (result.empty()) {
|
|
return result;
|
|
}
|
|
const auto border = TableBorder(bordered, st);
|
|
auto spanned = std::vector<TableCellMinimumWidthConstraint>();
|
|
for (const auto &constraint : constraints) {
|
|
const auto from = std::clamp(constraint.column, 0, columnCount);
|
|
const auto to = std::clamp(
|
|
constraint.column + constraint.colspan,
|
|
0,
|
|
columnCount);
|
|
if (from >= to) {
|
|
continue;
|
|
} else if ((to - from) == 1) {
|
|
result[from] = std::max(result[from], constraint.minimumWidth);
|
|
} else {
|
|
spanned.push_back(constraint);
|
|
}
|
|
}
|
|
std::sort(
|
|
spanned.begin(),
|
|
spanned.end(),
|
|
[](const TableCellMinimumWidthConstraint &a,
|
|
const TableCellMinimumWidthConstraint &b) {
|
|
return (a.colspan < b.colspan)
|
|
|| ((a.colspan == b.colspan) && (a.column < b.column));
|
|
});
|
|
for (const auto &constraint : spanned) {
|
|
const auto from = std::clamp(constraint.column, 0, columnCount);
|
|
const auto to = std::clamp(
|
|
constraint.column + constraint.colspan,
|
|
0,
|
|
columnCount);
|
|
const auto current = TableSpanWidth(
|
|
result,
|
|
from,
|
|
to - from,
|
|
border);
|
|
const auto deficit = constraint.minimumWidth - current;
|
|
if (deficit > 0) {
|
|
DistributeSpanDelta(&result, from, to, deficit);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int TableGridWidth(
|
|
const std::vector<int> &columnWidths,
|
|
const style::Markdown &st,
|
|
bool bordered) {
|
|
if (columnWidths.empty()) {
|
|
return 0;
|
|
}
|
|
const auto border = TableBorder(bordered, st);
|
|
auto result = border;
|
|
for (const auto columnWidth : columnWidths) {
|
|
result += columnWidth + border;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
int TableBlockContentMinimumWidth(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> &formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
LayoutContext context) {
|
|
const auto captionMinimum = prepared.text.text.isEmpty()
|
|
? 1
|
|
: [&] {
|
|
const auto base = FlowBlockMinimumWidth(prepared, st);
|
|
const auto minResizeWidth = FlowTextMinResizeWidth(st.body);
|
|
return WithCachedTextLeaf(
|
|
context,
|
|
BlockCachedTextLeafKey(
|
|
CachedTextLeafSlot::Leaf,
|
|
prepared,
|
|
context.preparedPath),
|
|
MarkedTextLeafSourceSignature(
|
|
prepared.text,
|
|
st.body,
|
|
minResizeWidth),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetTextLeaf(
|
|
leaf,
|
|
st.body,
|
|
st,
|
|
prepared.text,
|
|
&formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
minResizeWidth,
|
|
context.repaint,
|
|
context.repaintRect);
|
|
BindLinks(leaf, prepared.links);
|
|
},
|
|
[&](const Ui::Text::String &leaf,
|
|
Spellchecker::HighlightProcessId) {
|
|
return std::max(base, LeafMinimumWidth(leaf));
|
|
});
|
|
}();
|
|
const auto columnCount = prepared.tableColumnCount;
|
|
if (columnCount <= 0 || prepared.tableRows.empty()) {
|
|
return std::max(
|
|
captionMinimum,
|
|
TableMinimumGridWidth(columnCount, st, prepared.tableBordered));
|
|
}
|
|
const auto &padding = st.table.cellPadding;
|
|
const auto paddingWidth = padding.left() + padding.right();
|
|
auto constraints = std::vector<TableCellMinimumWidthConstraint>();
|
|
for (auto rowIndex = 0, rowCount = int(prepared.tableRows.size());
|
|
rowIndex != rowCount;
|
|
++rowIndex) {
|
|
const auto &row = prepared.tableRows[rowIndex];
|
|
for (auto cellIndex = 0, cellCount = int(row.cells.size());
|
|
cellIndex != cellCount;
|
|
++cellIndex) {
|
|
const auto &cell = row.cells[cellIndex];
|
|
const auto usePlaceholder = cell.text.text.isEmpty()
|
|
&& !cell.editPlaceholderText.isEmpty();
|
|
if (cell.text.text.isEmpty() && !usePlaceholder) {
|
|
continue;
|
|
}
|
|
const auto &textStyle = TableCellTextStyle(cell, st);
|
|
const auto minResizeWidth = TableCellTextMinResizeWidth(
|
|
textStyle,
|
|
st);
|
|
const auto leafMinimum = usePlaceholder
|
|
? WithCachedTextLeaf(
|
|
context,
|
|
TableCellCachedTextLeafKey(
|
|
CachedTextLeafSlot::TableCellPlaceholder,
|
|
cell,
|
|
context.preparedPath,
|
|
rowIndex,
|
|
cellIndex),
|
|
PlainTextLeafSourceSignature(
|
|
cell.editPlaceholderText,
|
|
textStyle,
|
|
minResizeWidth),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetPlainTextLeaf(
|
|
leaf,
|
|
textStyle,
|
|
cell.editPlaceholderText,
|
|
minResizeWidth);
|
|
},
|
|
[](const Ui::Text::String &leaf,
|
|
Spellchecker::HighlightProcessId) {
|
|
return LeafMinimumWidth(leaf);
|
|
})
|
|
: WithCachedTextLeaf(
|
|
context,
|
|
TableCellCachedTextLeafKey(
|
|
CachedTextLeafSlot::TableCellText,
|
|
cell,
|
|
context.preparedPath,
|
|
rowIndex,
|
|
cellIndex),
|
|
MarkedTextLeafSourceSignature(
|
|
cell.text,
|
|
textStyle,
|
|
minResizeWidth),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetTextLeaf(
|
|
leaf,
|
|
textStyle,
|
|
st,
|
|
cell.text,
|
|
&formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
minResizeWidth,
|
|
context.repaint,
|
|
context.repaintRect);
|
|
BindLinks(leaf, cell.links);
|
|
},
|
|
[](const Ui::Text::String &leaf,
|
|
Spellchecker::HighlightProcessId) {
|
|
return LeafMinimumWidth(leaf);
|
|
});
|
|
if (leafMinimum > 0) {
|
|
constraints.push_back({
|
|
.column = std::max(cell.column, 0),
|
|
.colspan = std::max(cell.colspan, 1),
|
|
.minimumWidth = std::max(
|
|
leafMinimum + paddingWidth,
|
|
st.table.minColumnWidth),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const auto columns = ComputeTableColumnMinimumWidths(
|
|
std::move(constraints),
|
|
columnCount,
|
|
st,
|
|
prepared.tableBordered);
|
|
return std::max(
|
|
captionMinimum,
|
|
TableGridWidth(columns, st, prepared.tableBordered));
|
|
}
|
|
|
|
int RetainedTableBlockMinimumWidth(
|
|
const PreparedBlock &prepared,
|
|
const LaidOutBlock &block,
|
|
const style::Markdown &st) {
|
|
const auto &captionLeaf = block.placeholderLeaf.isEmpty()
|
|
? block.leaf
|
|
: block.placeholderLeaf;
|
|
const auto captionMinimum = prepared.text.text.isEmpty()
|
|
? 1
|
|
: std::max(
|
|
FlowBlockMinimumWidth(prepared, st),
|
|
LeafMinimumWidth(captionLeaf));
|
|
const auto columnCount = prepared.tableColumnCount;
|
|
if (columnCount <= 0 || block.tableRows.empty()) {
|
|
return std::max(
|
|
captionMinimum,
|
|
TableMinimumGridWidth(columnCount, st, prepared.tableBordered));
|
|
}
|
|
const auto &padding = st.table.cellPadding;
|
|
const auto paddingWidth = padding.left() + padding.right();
|
|
auto constraints = std::vector<TableCellMinimumWidthConstraint>();
|
|
const auto rowCount = int(std::min(
|
|
prepared.tableRows.size(),
|
|
block.tableRows.size()));
|
|
for (auto rowIndex = 0; rowIndex != rowCount; ++rowIndex) {
|
|
const auto &preparedCells = prepared.tableRows[rowIndex].cells;
|
|
const auto &cells = block.tableRows[rowIndex].cells;
|
|
const auto cellCount = int(std::min(
|
|
preparedCells.size(),
|
|
cells.size()));
|
|
for (auto cellIndex = 0; cellIndex != cellCount; ++cellIndex) {
|
|
const auto &preparedCell = preparedCells[cellIndex];
|
|
const auto &cell = cells[cellIndex];
|
|
const auto usePlaceholder = preparedCell.text.text.isEmpty()
|
|
&& !preparedCell.editPlaceholderText.isEmpty();
|
|
const auto &displayLeaf = usePlaceholder
|
|
? cell.placeholderLeaf
|
|
: cell.leaf;
|
|
const auto leafMinimum = LeafMinimumWidth(displayLeaf);
|
|
if (leafMinimum > 0) {
|
|
constraints.push_back({
|
|
.column = cell.column,
|
|
.colspan = cell.colspan,
|
|
.minimumWidth = std::max(
|
|
leafMinimum + paddingWidth,
|
|
st.table.minColumnWidth),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const auto columns = ComputeTableColumnMinimumWidths(
|
|
std::move(constraints),
|
|
columnCount,
|
|
st,
|
|
prepared.tableBordered);
|
|
return std::max(
|
|
captionMinimum,
|
|
TableGridWidth(columns, st, prepared.tableBordered));
|
|
}
|
|
|
|
int BlockSkip(
|
|
const PreparedBlock &block,
|
|
const style::Markdown &st) {
|
|
if (IsAnchorOnlyBlock(block)) {
|
|
return 0;
|
|
}
|
|
const auto &skips = st.blockSkips;
|
|
switch (block.kind) {
|
|
case PreparedBlockKind::Paragraph:
|
|
case PreparedBlockKind::Thinking:
|
|
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::RelatedArticle:
|
|
return skips.relatedArticle;
|
|
case PreparedBlockKind::EmbedPost:
|
|
return skips.embedPost;
|
|
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 &st) {
|
|
if (previous.kind == PreparedBlockKind::Details
|
|
&& block.kind == PreparedBlockKind::Details) {
|
|
return 0;
|
|
}
|
|
if (context.tightList
|
|
&& IsFlowKind(previous.kind)
|
|
&& IsFlowKind(block.kind)) {
|
|
return 0;
|
|
}
|
|
return BlockSkip(block, st);
|
|
}
|
|
|
|
const style::TextStyle &TextStyleFor(
|
|
const PreparedBlock &block,
|
|
const style::Markdown &st) {
|
|
if (block.kind == PreparedBlockKind::CodeBlock) {
|
|
return st.code;
|
|
} else if (block.quoteAuthor) {
|
|
return st.quoteAuthorStyle;
|
|
} else if (block.kind != PreparedBlockKind::Heading) {
|
|
return st.body;
|
|
}
|
|
switch (std::clamp(block.headingLevel, 1, 6)) {
|
|
case 1: return st.heading1;
|
|
case 2: return st.heading2;
|
|
case 3: return st.heading3;
|
|
case 4: return st.heading4;
|
|
case 5: return st.heading5;
|
|
case 6: return st.heading6;
|
|
}
|
|
return st.heading6;
|
|
}
|
|
|
|
const style::TextStyle &EditPlaceholderTextStyleFor(
|
|
const PreparedBlock &block,
|
|
const style::Markdown &st) {
|
|
return block.quoteAuthor ? st.body : TextStyleFor(block, st);
|
|
}
|
|
|
|
void ApplyPreparedEditSources(
|
|
LaidOutBlock *block,
|
|
const PreparedBlock &prepared) {
|
|
block->editBlock = prepared.editBlock;
|
|
block->editListItem = prepared.editListItem;
|
|
block->editLeaf = prepared.editLeaf;
|
|
}
|
|
|
|
void RepopulateCodeBlockLeaf(
|
|
LaidOutBlock &block,
|
|
const std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
bool allowAsyncSyntaxHighlighting,
|
|
CodeBlockSyntaxHighlightTracker *syntaxHighlightTracker,
|
|
Fn<void()> repaint,
|
|
Fn<void(QRect)> repaintRect,
|
|
Fn<bool(const ClickContext&)> spoilerLinkFilter) {
|
|
PopulateCodeBlockLeaf(
|
|
&block.leaf,
|
|
&block.syntaxHighlightProcessId,
|
|
block.codeText,
|
|
block.codeLinks,
|
|
block.codeLanguage,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
allowAsyncSyntaxHighlighting,
|
|
syntaxHighlightTracker,
|
|
std::move(repaint),
|
|
std::move(repaintRect),
|
|
std::move(spoilerLinkFilter));
|
|
}
|
|
|
|
void UpdateLaidOutLeafContent(
|
|
LaidOutBlock *block,
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
LayoutContext context) {
|
|
if (!block) {
|
|
return;
|
|
}
|
|
block->placeholderText = QString();
|
|
block->placeholderLeaf = Ui::Text::String();
|
|
block->fallbackLeaf = Ui::Text::String();
|
|
switch (prepared.kind) {
|
|
case PreparedBlockKind::Paragraph:
|
|
case PreparedBlockKind::Thinking:
|
|
case PreparedBlockKind::Heading: {
|
|
const auto &textStyle = TextStyleFor(prepared, st);
|
|
const auto &placeholderStyle = EditPlaceholderTextStyleFor(
|
|
prepared,
|
|
st);
|
|
BuildOrReuseMarkedTextLeaf(
|
|
&block->leaf,
|
|
CachedTextLeafSlot::Leaf,
|
|
prepared,
|
|
textStyle,
|
|
st,
|
|
prepared.text,
|
|
prepared.links,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
FlowBlockMinimumWidth(prepared, st),
|
|
context);
|
|
if (prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty()) {
|
|
BuildOrReuseEditPlaceholderLeaf(
|
|
&block->placeholderText,
|
|
&block->placeholderLeaf,
|
|
prepared,
|
|
prepared.editPlaceholderText,
|
|
placeholderStyle,
|
|
PlainTextMinResizeWidth(placeholderStyle),
|
|
context);
|
|
}
|
|
} break;
|
|
case PreparedBlockKind::CodeBlock:
|
|
block->codeText = prepared.text;
|
|
block->codeLinks = prepared.links;
|
|
block->copyText = prepared.text.text;
|
|
block->codeLanguage = prepared.codeLanguage;
|
|
RepopulateCodeBlockLeaf(
|
|
*block,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
context.allowAsyncSyntaxHighlighting,
|
|
context.syntaxHighlightTracker,
|
|
context.repaint,
|
|
context.repaintRect,
|
|
context.spoilerLinkFilter);
|
|
if (prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty()) {
|
|
BuildOrReuseEditPlaceholderLeaf(
|
|
&block->placeholderText,
|
|
&block->placeholderLeaf,
|
|
prepared,
|
|
prepared.editPlaceholderText,
|
|
st.code,
|
|
PlainTextMinResizeWidth(st.code),
|
|
context);
|
|
}
|
|
break;
|
|
case PreparedBlockKind::DisplayMath:
|
|
block->copyText = prepared.formulaTex;
|
|
block->formulaIndex = prepared.formulaIndex;
|
|
if (const auto formula = formulas
|
|
? PreparedFormulaFor(*formulas, prepared.formulaIndex)
|
|
: nullptr;
|
|
!formula || !formula->measured.success) {
|
|
if (prepared.formulaTex.trimmed().isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty()) {
|
|
BuildOrReuseEditPlaceholderLeaf(
|
|
&block->placeholderText,
|
|
&block->placeholderLeaf,
|
|
prepared,
|
|
prepared.editPlaceholderText,
|
|
st.displayMath.fallbackStyle,
|
|
DisplayMathFallbackTextMinResizeWidth(st),
|
|
context);
|
|
} else {
|
|
BuildOrReuseMarkedTextLeaf(
|
|
&block->fallbackLeaf,
|
|
CachedTextLeafSlot::Fallback,
|
|
prepared,
|
|
st.displayMath.fallbackStyle,
|
|
st,
|
|
DisplayMathFallbackText(),
|
|
{},
|
|
nullptr,
|
|
nullptr,
|
|
nullptr,
|
|
DisplayMathFallbackTextMinResizeWidth(st),
|
|
context);
|
|
}
|
|
}
|
|
break;
|
|
case PreparedBlockKind::Table:
|
|
case PreparedBlockKind::Photo:
|
|
case PreparedBlockKind::Video:
|
|
case PreparedBlockKind::Audio:
|
|
case PreparedBlockKind::Map:
|
|
case PreparedBlockKind::Channel:
|
|
case PreparedBlockKind::GroupedMedia:
|
|
case PreparedBlockKind::EmbedPost:
|
|
BuildOrReuseMarkedTextLeaf(
|
|
&block->leaf,
|
|
CachedTextLeafSlot::Leaf,
|
|
prepared,
|
|
st.body,
|
|
st,
|
|
prepared.text,
|
|
prepared.links,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
FlowTextMinResizeWidth(st.body),
|
|
context);
|
|
if (prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty()) {
|
|
BuildOrReuseEditPlaceholderLeaf(
|
|
&block->placeholderText,
|
|
&block->placeholderLeaf,
|
|
prepared,
|
|
prepared.editPlaceholderText,
|
|
st.body,
|
|
PlainTextMinResizeWidth(st.body),
|
|
context);
|
|
}
|
|
break;
|
|
case PreparedBlockKind::Details:
|
|
BuildOrReuseMarkedTextLeaf(
|
|
&block->leaf,
|
|
CachedTextLeafSlot::Leaf,
|
|
prepared,
|
|
st.details.summaryStyle,
|
|
st,
|
|
prepared.text,
|
|
prepared.links,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
FlowTextMinResizeWidth(st.details.summaryStyle),
|
|
context);
|
|
if (prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty()) {
|
|
BuildOrReuseEditPlaceholderLeaf(
|
|
&block->placeholderText,
|
|
&block->placeholderLeaf,
|
|
prepared,
|
|
prepared.editPlaceholderText,
|
|
st.details.summaryStyle,
|
|
PlainTextMinResizeWidth(st.details.summaryStyle),
|
|
context);
|
|
}
|
|
break;
|
|
case PreparedBlockKind::Rule:
|
|
case PreparedBlockKind::List:
|
|
case PreparedBlockKind::ListItem:
|
|
case PreparedBlockKind::Quote:
|
|
case PreparedBlockKind::Placeholder:
|
|
case PreparedBlockKind::RelatedArticle:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void UpdateLaidOutLeafContent(
|
|
LaidOutTableCell *cell,
|
|
const PreparedTableCell &prepared,
|
|
const std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
int tableRowIndex,
|
|
int tableCellIndex,
|
|
LayoutContext context) {
|
|
if (!cell) {
|
|
return;
|
|
}
|
|
const auto &textStyle = TableCellTextStyle(prepared, st);
|
|
const auto minResizeWidth = TableCellTextMinResizeWidth(textStyle, st);
|
|
cell->header = prepared.header;
|
|
cell->verticalAlignment = prepared.verticalAlignment;
|
|
cell->align = CellAlign(prepared.alignment);
|
|
cell->column = std::max(prepared.column, 0);
|
|
cell->colspan = std::max(prepared.colspan, 1);
|
|
cell->rowspan = std::max(prepared.rowspan, 1);
|
|
cell->placeholderText = QString();
|
|
cell->placeholderLeaf = Ui::Text::String();
|
|
cell->cachedPreferredWidth = -1;
|
|
cell->cachedPreferredHeight = 0;
|
|
BuildOrReuseCachedTextLeaf(
|
|
&cell->leaf,
|
|
nullptr,
|
|
context,
|
|
TableCellCachedTextLeafKey(
|
|
CachedTextLeafSlot::TableCellText,
|
|
prepared,
|
|
context.preparedPath,
|
|
tableRowIndex,
|
|
tableCellIndex),
|
|
MarkedTextLeafSourceSignature(prepared.text, textStyle, minResizeWidth),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetTextLeaf(
|
|
leaf,
|
|
textStyle,
|
|
st,
|
|
prepared.text,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
minResizeWidth,
|
|
context.repaint,
|
|
context.repaintRect);
|
|
BindLinks(leaf, prepared.links);
|
|
});
|
|
BindLinks(&cell->leaf, prepared.links);
|
|
if (prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty()) {
|
|
cell->placeholderText = prepared.editPlaceholderText;
|
|
BuildOrReuseCachedTextLeaf(
|
|
&cell->placeholderLeaf,
|
|
nullptr,
|
|
context,
|
|
TableCellCachedTextLeafKey(
|
|
CachedTextLeafSlot::TableCellPlaceholder,
|
|
prepared,
|
|
context.preparedPath,
|
|
tableRowIndex,
|
|
tableCellIndex),
|
|
PlainTextLeafSourceSignature(
|
|
cell->placeholderText,
|
|
textStyle,
|
|
minResizeWidth),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId*) {
|
|
SetPlainTextLeaf(
|
|
leaf,
|
|
textStyle,
|
|
cell->placeholderText,
|
|
minResizeWidth);
|
|
});
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] std::optional<int> LayoutFlowBlockGeometry(
|
|
const PreparedBlock &prepared,
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
bool scrollOwner,
|
|
LayoutContext context);
|
|
[[nodiscard]] std::optional<int> LayoutCodeBlockGeometry(
|
|
const PreparedBlock &prepared,
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
bool scrollOwner,
|
|
LayoutContext context);
|
|
[[nodiscard]] std::optional<int> LayoutRuleBlockGeometry(
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width);
|
|
[[nodiscard]] std::optional<int> LayoutDisplayMathBlockGeometry(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> &formulas,
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
bool scrollOwner,
|
|
LayoutContext context);
|
|
[[nodiscard]] std::optional<int> LayoutTableBlockGeometry(
|
|
const PreparedBlock &prepared,
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
bool scrollOwner,
|
|
LayoutContext context);
|
|
[[nodiscard]] std::optional<int> LayoutPlaceholderBlockGeometry(
|
|
const PreparedBlock &prepared,
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
LayoutContext context);
|
|
[[nodiscard]] std::optional<int> LayoutRelatedArticleBlockGeometry(
|
|
const PreparedBlock &prepared,
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
LayoutContext context);
|
|
[[nodiscard]] std::optional<int> LayoutFramedMediaBlockGeometry(
|
|
LaidOutBlock *block,
|
|
const PreparedBlock &prepared,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
QMargins padding,
|
|
int captionSkip,
|
|
bool limitWidth,
|
|
bool centerMedia,
|
|
int intrinsicWidth,
|
|
LayoutContext context);
|
|
[[nodiscard]] std::optional<int> LayoutCardMediaBlockGeometry(
|
|
LaidOutBlock *block,
|
|
const PreparedBlock &prepared,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
QMargins padding,
|
|
int captionSkip,
|
|
LayoutContext context);
|
|
|
|
LaidOutBlock LayoutFlowBlock(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
bool scrollOwner,
|
|
LayoutContext context) {
|
|
auto block = LaidOutBlock();
|
|
ApplyPreparedEditSources(&block, prepared);
|
|
block.kind = prepared.kind;
|
|
block.anchorId = prepared.anchorId;
|
|
block.anchorIds = prepared.anchorIds;
|
|
block.headingLevel = prepared.headingLevel;
|
|
block.supplementary = prepared.supplementary;
|
|
block.pullquote = prepared.pullquote;
|
|
block.quoteAuthor = prepared.quoteAuthor;
|
|
block.flowTextAlign = CellAlign(prepared.flowAlignment);
|
|
const auto &textStyle = TextStyleFor(prepared, st);
|
|
const auto &placeholderStyle = EditPlaceholderTextStyleFor(prepared, st);
|
|
if (!IsAnchorOnlyBlock(prepared)) {
|
|
BuildOrReuseMarkedTextLeaf(
|
|
&block.leaf,
|
|
CachedTextLeafSlot::Leaf,
|
|
prepared,
|
|
textStyle,
|
|
st,
|
|
prepared.text,
|
|
prepared.links,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
FlowBlockMinimumWidth(prepared, st),
|
|
context);
|
|
const auto usePlaceholder = prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty();
|
|
if (usePlaceholder) {
|
|
BuildOrReuseEditPlaceholderLeaf(
|
|
&block.placeholderText,
|
|
&block.placeholderLeaf,
|
|
prepared,
|
|
prepared.editPlaceholderText,
|
|
placeholderStyle,
|
|
PlainTextMinResizeWidth(placeholderStyle),
|
|
context);
|
|
}
|
|
}
|
|
const auto bottom = LayoutFlowBlockGeometry(
|
|
prepared,
|
|
&block,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
logicalWidth,
|
|
scrollOwner,
|
|
context);
|
|
Expects(bottom.has_value());
|
|
return FinalizeLaidOutBlock(std::move(block));
|
|
}
|
|
|
|
LaidOutBlock LayoutCodeBlock(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
bool scrollOwner,
|
|
bool allowAsyncSyntaxHighlighting,
|
|
CodeBlockSyntaxHighlightTracker *syntaxHighlightTracker,
|
|
LayoutContext context) {
|
|
auto block = LaidOutBlock();
|
|
ApplyPreparedEditSources(&block, prepared);
|
|
block.kind = PreparedBlockKind::CodeBlock;
|
|
block.anchorId = prepared.anchorId;
|
|
block.anchorIds = prepared.anchorIds;
|
|
block.codeText = prepared.text;
|
|
block.codeLinks = prepared.links;
|
|
block.copyText = block.codeText.text;
|
|
block.codeLanguage = prepared.codeLanguage;
|
|
BuildOrReuseCachedTextLeaf(
|
|
&block.leaf,
|
|
&block.syntaxHighlightProcessId,
|
|
context,
|
|
BlockCachedTextLeafKey(
|
|
CachedTextLeafSlot::Leaf,
|
|
prepared,
|
|
context.preparedPath),
|
|
CodeTextLeafSourceSignature(prepared, st),
|
|
[&](Ui::Text::String *leaf,
|
|
Spellchecker::HighlightProcessId *syntaxHighlightProcessId) {
|
|
PopulateCodeBlockLeaf(
|
|
leaf,
|
|
syntaxHighlightProcessId,
|
|
block.codeText,
|
|
block.codeLinks,
|
|
block.codeLanguage,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
allowAsyncSyntaxHighlighting,
|
|
syntaxHighlightTracker,
|
|
context.repaint,
|
|
context.repaintRect,
|
|
context.spoilerLinkFilter);
|
|
});
|
|
BindLinks(&block.leaf, block.codeLinks);
|
|
if (!block.syntaxHighlightProcessId
|
|
&& allowAsyncSyntaxHighlighting
|
|
&& syntaxHighlightTracker) {
|
|
auto highlightRequest = TextWithEntities();
|
|
highlightRequest.text = CodeBlockDisplayText(block.codeText).text;
|
|
if (!highlightRequest.text.isEmpty()) {
|
|
highlightRequest.entities.push_back(EntityInText(
|
|
EntityType::Pre,
|
|
0,
|
|
highlightRequest.text.size(),
|
|
block.codeLanguage));
|
|
}
|
|
block.syntaxHighlightProcessId = syntaxHighlightTracker->tryHighlightSyntax(
|
|
highlightRequest.text,
|
|
block.codeLanguage,
|
|
highlightRequest);
|
|
}
|
|
const auto usePlaceholder = prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty();
|
|
if (usePlaceholder) {
|
|
BuildOrReuseEditPlaceholderLeaf(
|
|
&block.placeholderText,
|
|
&block.placeholderLeaf,
|
|
prepared,
|
|
prepared.editPlaceholderText,
|
|
st.code,
|
|
PlainTextMinResizeWidth(st.code),
|
|
context);
|
|
}
|
|
const auto bottom = LayoutCodeBlockGeometry(
|
|
prepared,
|
|
&block,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
logicalWidth,
|
|
scrollOwner,
|
|
context);
|
|
Expects(bottom.has_value());
|
|
return FinalizeLaidOutBlock(std::move(block));
|
|
}
|
|
|
|
LaidOutBlock LayoutRuleBlock(
|
|
const PreparedBlock &prepared,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width) {
|
|
auto block = LaidOutBlock();
|
|
ApplyPreparedEditSources(&block, prepared);
|
|
block.kind = PreparedBlockKind::Rule;
|
|
const auto bottom = LayoutRuleBlockGeometry(
|
|
&block,
|
|
st,
|
|
left,
|
|
top,
|
|
width);
|
|
Expects(bottom.has_value());
|
|
return FinalizeLaidOutBlock(std::move(block));
|
|
}
|
|
|
|
LaidOutBlock LayoutDisplayMathBlock(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> &formulas,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
bool scrollOwner,
|
|
LayoutContext context) {
|
|
auto block = LaidOutBlock();
|
|
ApplyPreparedEditSources(&block, prepared);
|
|
block.kind = PreparedBlockKind::DisplayMath;
|
|
block.anchorId = prepared.anchorId;
|
|
block.anchorIds = prepared.anchorIds;
|
|
block.formulaIndex = prepared.formulaIndex;
|
|
block.copyText = prepared.formulaTex;
|
|
|
|
const auto formula = PreparedFormulaFor(formulas, prepared.formulaIndex);
|
|
const auto usePlaceholder = prepared.formulaTex.trimmed().isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty();
|
|
if (!(formula && formula->measured.success)) {
|
|
if (usePlaceholder) {
|
|
BuildOrReuseEditPlaceholderLeaf(
|
|
&block.placeholderText,
|
|
&block.placeholderLeaf,
|
|
prepared,
|
|
prepared.editPlaceholderText,
|
|
st.displayMath.fallbackStyle,
|
|
DisplayMathFallbackTextMinResizeWidth(st),
|
|
context);
|
|
} else {
|
|
BuildOrReuseMarkedTextLeaf(
|
|
&block.fallbackLeaf,
|
|
CachedTextLeafSlot::Fallback,
|
|
prepared,
|
|
st.displayMath.fallbackStyle,
|
|
st,
|
|
DisplayMathFallbackText(),
|
|
{},
|
|
nullptr,
|
|
nullptr,
|
|
nullptr,
|
|
DisplayMathFallbackTextMinResizeWidth(st),
|
|
context);
|
|
}
|
|
}
|
|
const auto bottom = LayoutDisplayMathBlockGeometry(
|
|
prepared,
|
|
formulas,
|
|
&block,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
logicalWidth,
|
|
scrollOwner,
|
|
context);
|
|
Expects(bottom.has_value());
|
|
return FinalizeLaidOutBlock(std::move(block));
|
|
}
|
|
|
|
LaidOutBlock LayoutTableBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
bool scrollOwner,
|
|
LayoutContext context) {
|
|
auto block = LaidOutBlock();
|
|
ApplyPreparedEditSources(&block, prepared);
|
|
block.kind = PreparedBlockKind::Table;
|
|
block.anchorId = prepared.anchorId;
|
|
block.anchorIds = prepared.anchorIds;
|
|
block.tableBordered = prepared.tableBordered;
|
|
block.tableStriped = prepared.tableStriped;
|
|
block.supplementary = prepared.supplementary;
|
|
block.flowTextAlign = style::al_center;
|
|
|
|
FillMediaCaption(
|
|
&block,
|
|
prepared,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
context);
|
|
|
|
block.tableRows.reserve(prepared.tableRows.size());
|
|
for (auto rowIndex = 0, rowCount = int(prepared.tableRows.size());
|
|
rowIndex != rowCount;
|
|
++rowIndex) {
|
|
const auto &preparedRow = prepared.tableRows[rowIndex];
|
|
auto row = LaidOutTableRow();
|
|
row.header = preparedRow.header;
|
|
row.editRow = preparedRow.editRow;
|
|
row.cells.reserve(preparedRow.cells.size());
|
|
for (auto cellIndex = 0, cellCount = int(preparedRow.cells.size());
|
|
cellIndex != cellCount;
|
|
++cellIndex) {
|
|
auto cell = InitializeTableCellLayout(
|
|
preparedRow.cells[cellIndex],
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
rowIndex,
|
|
cellIndex,
|
|
context);
|
|
row.cells.push_back(std::move(cell));
|
|
}
|
|
block.tableRows.push_back(std::move(row));
|
|
}
|
|
|
|
const auto bottom = LayoutTableBlockGeometry(
|
|
prepared,
|
|
&block,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
logicalWidth,
|
|
scrollOwner,
|
|
context);
|
|
Expects(bottom.has_value());
|
|
return FinalizeLaidOutBlock(std::move(block));
|
|
}
|
|
|
|
LaidOutBlock LayoutPlaceholderBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
LayoutContext context) {
|
|
auto block = LaidOutBlock();
|
|
ApplyPreparedEditSources(&block, prepared);
|
|
block.kind = PreparedBlockKind::Placeholder;
|
|
block.anchorId = prepared.anchorId;
|
|
block.anchorIds = prepared.anchorIds;
|
|
block.copyText = prepared.placeholder.copyText;
|
|
block.labelText = prepared.placeholder.label;
|
|
block.placeholderId = prepared.placeholder.id;
|
|
if (block.placeholderId && context.placeholderRuntimeFactory) {
|
|
block.placeholderRuntime = context.placeholderRuntimeFactory(
|
|
block.placeholderId);
|
|
}
|
|
block.supplementary = prepared.supplementary;
|
|
|
|
const auto &style = st.placeholder;
|
|
BuildOrReusePlainTextLeaf(
|
|
&block.labelLeaf,
|
|
CachedTextLeafSlot::Label,
|
|
prepared,
|
|
style.labelStyle,
|
|
block.labelText,
|
|
PlainTextMinResizeWidth(style.labelStyle),
|
|
context);
|
|
if (prepared.placeholder.embed) {
|
|
block.activation.kind = MediaActivationKind::Embed;
|
|
block.activation.embed = *prepared.placeholder.embed;
|
|
block.activation.placeholderId = block.placeholderId;
|
|
}
|
|
FillMediaCaption(
|
|
&block,
|
|
prepared,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
context);
|
|
const auto bottom = LayoutPlaceholderBlockGeometry(
|
|
prepared,
|
|
&block,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
context);
|
|
Expects(bottom.has_value());
|
|
return FinalizeLaidOutBlock(std::move(block));
|
|
}
|
|
|
|
LaidOutBlock LayoutRelatedArticleBlock(
|
|
const PreparedBlock &prepared,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
LayoutContext context) {
|
|
auto block = LaidOutBlock();
|
|
ApplyPreparedEditSources(&block, prepared);
|
|
block.kind = PreparedBlockKind::RelatedArticle;
|
|
block.anchorId = prepared.anchorId;
|
|
block.anchorIds = prepared.anchorIds;
|
|
block.copyText = prepared.relatedArticle.copyText;
|
|
block.labelText = prepared.relatedArticle.title;
|
|
block.preparedLink = prepared.relatedArticle.link;
|
|
block.preparedLinkHandler = CreatePreparedLinkHandler(
|
|
prepared.relatedArticle.link);
|
|
if (prepared.relatedArticle.photoId && mediaRuntime) {
|
|
block.photoRuntime = mediaRuntime->resolvePhoto(
|
|
prepared.relatedArticle.photoId);
|
|
}
|
|
|
|
const auto &card = st.relatedArticle;
|
|
block.thumbnailPhotoId = prepared.relatedArticle.photoId;
|
|
if (!prepared.relatedArticle.title.isEmpty()) {
|
|
BuildOrReusePlainTextLeaf(
|
|
&block.labelLeaf,
|
|
CachedTextLeafSlot::Label,
|
|
prepared,
|
|
card.titleStyle,
|
|
prepared.relatedArticle.title,
|
|
PlainTextMinResizeWidth(card.titleStyle),
|
|
context);
|
|
}
|
|
if (!prepared.relatedArticle.description.isEmpty()) {
|
|
BuildOrReusePlainTextLeaf(
|
|
&block.subtitleLeaf,
|
|
CachedTextLeafSlot::Subtitle,
|
|
prepared,
|
|
card.subtitleStyle,
|
|
prepared.relatedArticle.description,
|
|
PlainTextMinResizeWidth(card.subtitleStyle),
|
|
context);
|
|
}
|
|
if (!prepared.relatedArticle.footer.isEmpty()) {
|
|
BuildOrReusePlainTextLeaf(
|
|
&block.actionLeaf,
|
|
CachedTextLeafSlot::Action,
|
|
prepared,
|
|
card.footerStyle,
|
|
prepared.relatedArticle.footer,
|
|
PlainTextMinResizeWidth(card.footerStyle),
|
|
context);
|
|
}
|
|
|
|
const auto bottom = LayoutRelatedArticleBlockGeometry(
|
|
prepared,
|
|
&block,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
context);
|
|
Expects(bottom.has_value());
|
|
return FinalizeLaidOutBlock(std::move(block));
|
|
}
|
|
|
|
LaidOutBlock LayoutPhotoBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
LayoutContext context) {
|
|
auto block = LaidOutBlock();
|
|
ApplyPreparedEditSources(&block, prepared);
|
|
block.kind = PreparedBlockKind::Photo;
|
|
block.anchorId = prepared.anchorId;
|
|
block.anchorIds = prepared.anchorIds;
|
|
block.supplementary = prepared.supplementary;
|
|
if (context.mediaBlockFactory) {
|
|
block.mediaBlock = context.mediaBlockFactory(prepared);
|
|
}
|
|
if (block.mediaBlock) {
|
|
block.copyText = block.mediaBlock->selectionData().copyText;
|
|
}
|
|
FillMediaCaption(
|
|
&block,
|
|
prepared,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
context);
|
|
const auto bottom = LayoutFramedMediaBlockGeometry(
|
|
&block,
|
|
prepared,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
st.photo.padding,
|
|
st.photo.captionSkip,
|
|
true,
|
|
true,
|
|
prepared.photo.width,
|
|
context);
|
|
Expects(bottom.has_value());
|
|
return FinalizeLaidOutBlock(std::move(block));
|
|
}
|
|
|
|
LaidOutBlock LayoutVideoBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
LayoutContext context) {
|
|
auto block = LaidOutBlock();
|
|
ApplyPreparedEditSources(&block, prepared);
|
|
block.kind = PreparedBlockKind::Video;
|
|
block.anchorId = prepared.anchorId;
|
|
block.anchorIds = prepared.anchorIds;
|
|
block.supplementary = prepared.supplementary;
|
|
if (context.mediaBlockFactory) {
|
|
block.mediaBlock = context.mediaBlockFactory(prepared);
|
|
}
|
|
if (block.mediaBlock) {
|
|
block.copyText = block.mediaBlock->selectionData().copyText;
|
|
}
|
|
FillMediaCaption(
|
|
&block,
|
|
prepared,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
context);
|
|
const auto bottom = LayoutFramedMediaBlockGeometry(
|
|
&block,
|
|
prepared,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
st.photo.padding,
|
|
st.photo.captionSkip,
|
|
true,
|
|
true,
|
|
prepared.video.media.width,
|
|
context);
|
|
Expects(bottom.has_value());
|
|
return FinalizeLaidOutBlock(std::move(block));
|
|
}
|
|
|
|
LaidOutBlock LayoutAudioBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
LayoutContext context) {
|
|
auto block = LaidOutBlock();
|
|
ApplyPreparedEditSources(&block, prepared);
|
|
block.kind = PreparedBlockKind::Audio;
|
|
block.anchorId = prepared.anchorId;
|
|
block.anchorIds = prepared.anchorIds;
|
|
block.supplementary = prepared.supplementary;
|
|
if (context.mediaBlockFactory) {
|
|
block.mediaBlock = context.mediaBlockFactory(prepared);
|
|
}
|
|
if (block.mediaBlock) {
|
|
block.copyText = block.mediaBlock->selectionData().copyText;
|
|
}
|
|
FillMediaCaption(
|
|
&block,
|
|
prepared,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
context);
|
|
const auto bottom = LayoutCardMediaBlockGeometry(
|
|
&block,
|
|
prepared,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
st.audio.padding,
|
|
st.audio.captionSkip,
|
|
context);
|
|
Expects(bottom.has_value());
|
|
return FinalizeLaidOutBlock(std::move(block));
|
|
}
|
|
|
|
LaidOutBlock LayoutMapBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
LayoutContext context) {
|
|
auto block = LaidOutBlock();
|
|
ApplyPreparedEditSources(&block, prepared);
|
|
block.kind = PreparedBlockKind::Map;
|
|
block.anchorId = prepared.anchorId;
|
|
block.anchorIds = prepared.anchorIds;
|
|
block.supplementary = prepared.supplementary;
|
|
if (context.mediaBlockFactory) {
|
|
block.mediaBlock = context.mediaBlockFactory(prepared);
|
|
}
|
|
if (block.mediaBlock) {
|
|
block.copyText = block.mediaBlock->selectionData().copyText;
|
|
}
|
|
FillMediaCaption(
|
|
&block,
|
|
prepared,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
context);
|
|
const auto bottom = LayoutFramedMediaBlockGeometry(
|
|
&block,
|
|
prepared,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
st.photo.padding,
|
|
st.photo.captionSkip,
|
|
false,
|
|
false,
|
|
0,
|
|
context);
|
|
Expects(bottom.has_value());
|
|
return FinalizeLaidOutBlock(std::move(block));
|
|
}
|
|
|
|
LaidOutBlock LayoutChannelBlock(
|
|
const PreparedBlock &prepared,
|
|
std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
LayoutContext context) {
|
|
auto block = LaidOutBlock();
|
|
ApplyPreparedEditSources(&block, prepared);
|
|
block.kind = PreparedBlockKind::Channel;
|
|
block.anchorId = prepared.anchorId;
|
|
block.anchorIds = prepared.anchorIds;
|
|
block.supplementary = prepared.supplementary;
|
|
if (context.mediaBlockFactory) {
|
|
block.mediaBlock = context.mediaBlockFactory(prepared);
|
|
}
|
|
if (block.mediaBlock) {
|
|
block.copyText = block.mediaBlock->selectionData().copyText;
|
|
}
|
|
FillMediaCaption(
|
|
&block,
|
|
prepared,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
context);
|
|
const auto bottom = LayoutCardMediaBlockGeometry(
|
|
&block,
|
|
prepared,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
st.channel.padding,
|
|
st.audio.captionSkip,
|
|
context);
|
|
Expects(bottom.has_value());
|
|
return FinalizeLaidOutBlock(std::move(block));
|
|
}
|
|
|
|
LaidOutBlock LayoutGroupedMediaBlock(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> *formulas,
|
|
InlineFormulaObjectCache *inlineFormulaObjects,
|
|
const std::shared_ptr<MediaRuntime> &mediaRuntime,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
LayoutContext context) {
|
|
auto block = LaidOutBlock();
|
|
ApplyPreparedEditSources(&block, prepared);
|
|
block.kind = PreparedBlockKind::GroupedMedia;
|
|
block.anchorId = prepared.anchorId;
|
|
block.anchorIds = prepared.anchorIds;
|
|
block.supplementary = prepared.supplementary;
|
|
if (context.mediaBlockFactory) {
|
|
block.mediaBlock = context.mediaBlockFactory(prepared);
|
|
}
|
|
if (block.mediaBlock) {
|
|
block.copyText = block.mediaBlock->selectionData().copyText;
|
|
}
|
|
FillMediaCaption(
|
|
&block,
|
|
prepared,
|
|
formulas,
|
|
inlineFormulaObjects,
|
|
mediaRuntime,
|
|
st,
|
|
context);
|
|
const auto bottom = LayoutFramedMediaBlockGeometry(
|
|
&block,
|
|
prepared,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
st.groupedMedia.padding,
|
|
st.groupedMedia.captionSkip,
|
|
false,
|
|
false,
|
|
0,
|
|
context);
|
|
Expects(bottom.has_value());
|
|
return FinalizeLaidOutBlock(std::move(block));
|
|
}
|
|
|
|
[[nodiscard]] std::optional<int> LayoutFlowBlockGeometry(
|
|
const PreparedBlock &prepared,
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
bool scrollOwner,
|
|
LayoutContext context) {
|
|
if (!block) {
|
|
return std::nullopt;
|
|
}
|
|
const auto usePlaceholder = prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty();
|
|
if (MissingRetainedLeaf(prepared.text.text, block->leaf)
|
|
|| MissingRetainedPlaceholderLeaf(usePlaceholder, block->placeholderLeaf)) {
|
|
return std::nullopt;
|
|
}
|
|
ClearBlockGeometry(block);
|
|
const auto visibleWidth = std::max(width, 1);
|
|
block->textWidth = std::max(
|
|
(logicalWidth > 0) ? logicalWidth : visibleWidth,
|
|
1);
|
|
if (IsAnchorOnlyBlock(prepared)) {
|
|
block->textRect = QRect(left, top, block->textWidth, 0);
|
|
block->contentRect = QRect(left, top, visibleWidth, 0);
|
|
block->outer = block->contentRect;
|
|
FinishBlockGeometry(block);
|
|
return block->outer.y() + block->outer.height();
|
|
}
|
|
const auto &textStyle = TextStyleFor(prepared, st);
|
|
const auto &displayLeaf = usePlaceholder
|
|
? block->placeholderLeaf
|
|
: block->leaf;
|
|
const auto height = ResolveEditableHeight(
|
|
LeafHeight(displayLeaf, textStyle, block->textWidth),
|
|
context);
|
|
block->textRect = QRect(left, top, block->textWidth, height);
|
|
block->contentRect = QRect(left, top, visibleWidth, height);
|
|
block->overflowed = (block->textWidth > visibleWidth);
|
|
block->horizontalScrollMax = scrollOwner
|
|
? std::max(block->textWidth - visibleWidth, 0)
|
|
: 0;
|
|
if (scrollOwner) {
|
|
block->scrollViewportRect = block->contentRect;
|
|
block->scrollLogicalContentRect = block->textRect;
|
|
if (block->horizontalScrollMax > 0) {
|
|
block->scrollScrollbarTrackRect = QRect(
|
|
left,
|
|
top + height + st.table.scrollbarSkip,
|
|
visibleWidth,
|
|
st.table.scrollbarHeight);
|
|
}
|
|
}
|
|
block->outer = QRect(
|
|
left,
|
|
top,
|
|
visibleWidth,
|
|
height + ScrollbarReserveHeight(
|
|
scrollOwner,
|
|
block->horizontalScrollMax,
|
|
st));
|
|
block->firstLineBaseline = LeafFirstLineBaseline(
|
|
displayLeaf,
|
|
block->textRect,
|
|
textStyle);
|
|
FinishBlockGeometry(block);
|
|
return block->outer.y() + block->outer.height();
|
|
}
|
|
|
|
[[nodiscard]] std::optional<int> LayoutCodeBlockGeometry(
|
|
const PreparedBlock &prepared,
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
bool scrollOwner,
|
|
LayoutContext context) {
|
|
if (!block) {
|
|
return std::nullopt;
|
|
}
|
|
const auto usePlaceholder = prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty();
|
|
if (MissingRetainedLeaf(prepared.text.text, block->leaf)
|
|
|| MissingRetainedPlaceholderLeaf(usePlaceholder, block->placeholderLeaf)) {
|
|
return std::nullopt;
|
|
}
|
|
ClearBlockGeometry(block);
|
|
const auto &pre = st.code.pre;
|
|
const auto padding = BlockquotePadding(pre);
|
|
const auto outerWidth = std::max(
|
|
width,
|
|
padding.left() + padding.right() + 1);
|
|
const auto logicalOuterWidth = std::max(
|
|
(logicalWidth > 0) ? logicalWidth : outerWidth,
|
|
padding.left() + padding.right() + 1);
|
|
const auto viewportWidth = std::max(
|
|
outerWidth - padding.left() - padding.right(),
|
|
1);
|
|
block->textWidth = std::max(
|
|
logicalOuterWidth - padding.left() - padding.right(),
|
|
1);
|
|
const auto &displayLeaf = usePlaceholder
|
|
? block->placeholderLeaf
|
|
: block->leaf;
|
|
const auto height = ResolveEditableHeight(
|
|
LeafHeight(displayLeaf, st.code, block->textWidth),
|
|
context);
|
|
block->overflowed = (block->textWidth > viewportWidth);
|
|
block->horizontalScrollMax = scrollOwner
|
|
? std::max(block->textWidth - viewportWidth, 0)
|
|
: 0;
|
|
const auto outerHeight = padding.top()
|
|
+ height
|
|
+ padding.bottom()
|
|
+ ScrollbarReserveHeight(
|
|
scrollOwner,
|
|
block->horizontalScrollMax,
|
|
st);
|
|
block->outer = QRect(left, top, outerWidth, outerHeight);
|
|
block->headerRect = QRect(left, top, outerWidth, pre.header);
|
|
block->bodyRect = QRect(
|
|
left,
|
|
top + pre.header,
|
|
outerWidth,
|
|
std::max(outerHeight - pre.header, 0));
|
|
block->contentRect = QRect(
|
|
left + padding.left(),
|
|
top + padding.top(),
|
|
viewportWidth,
|
|
height);
|
|
block->textRect = QRect(
|
|
left + padding.left(),
|
|
top + padding.top(),
|
|
block->textWidth,
|
|
height);
|
|
if (scrollOwner) {
|
|
block->scrollViewportRect = block->contentRect;
|
|
block->scrollLogicalContentRect = block->textRect;
|
|
if (block->horizontalScrollMax > 0) {
|
|
block->scrollScrollbarTrackRect = QRect(
|
|
block->contentRect.x(),
|
|
block->contentRect.y()
|
|
+ block->contentRect.height()
|
|
+ st.table.scrollbarSkip,
|
|
block->contentRect.width(),
|
|
st.table.scrollbarHeight);
|
|
}
|
|
}
|
|
block->firstLineBaseline = LeafFirstLineBaseline(
|
|
displayLeaf,
|
|
block->textRect,
|
|
st.code);
|
|
FinishBlockGeometry(block);
|
|
return block->outer.y() + block->outer.height();
|
|
}
|
|
|
|
[[nodiscard]] std::optional<int> LayoutRuleBlockGeometry(
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width) {
|
|
if (!block) {
|
|
return std::nullopt;
|
|
}
|
|
ClearBlockGeometry(block);
|
|
block->outer = QRect(left, top, std::max(width, 1), st.rule.height);
|
|
block->textRect = block->outer;
|
|
FinishBlockGeometry(block);
|
|
return block->outer.y() + block->outer.height();
|
|
}
|
|
|
|
[[nodiscard]] std::optional<int> LayoutDisplayMathBlockGeometry(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> &formulas,
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
bool scrollOwner,
|
|
LayoutContext context) {
|
|
if (!block) {
|
|
return std::nullopt;
|
|
}
|
|
const auto formula = PreparedFormulaFor(formulas, prepared.formulaIndex);
|
|
const auto usePlaceholder = prepared.formulaTex.trimmed().isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty();
|
|
if (!formula || !formula->measured.success) {
|
|
if (usePlaceholder) {
|
|
if (block->placeholderLeaf.isEmpty()) {
|
|
return std::nullopt;
|
|
}
|
|
} else if (block->fallbackLeaf.isEmpty()) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
ClearBlockGeometry(block);
|
|
const auto &padding = st.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 contentLogicalWidth = std::max(
|
|
((logicalWidth > 0) ? logicalWidth : std::max(width, 1))
|
|
- padding.left()
|
|
- padding.right(),
|
|
1);
|
|
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 = st.displayMath.fallbackPadding;
|
|
const auto fallbackPaddingWidth = fallbackPadding.left()
|
|
+ fallbackPadding.right();
|
|
const auto &displayLeaf = usePlaceholder
|
|
? block->placeholderLeaf
|
|
: block->fallbackLeaf;
|
|
block->textWidth = std::max(
|
|
contentLogicalWidth - fallbackPaddingWidth,
|
|
1);
|
|
block->textWidth = std::min(
|
|
block->textWidth,
|
|
std::max(displayLeaf.maxWidth(), 1));
|
|
auto textHeight = LeafHeight(
|
|
displayLeaf,
|
|
st.displayMath.fallbackStyle,
|
|
block->textWidth);
|
|
formulaWidth = std::min(
|
|
block->textWidth + fallbackPaddingWidth,
|
|
contentLogicalWidth);
|
|
block->textWidth = std::max(formulaWidth - fallbackPaddingWidth, 1);
|
|
textHeight = LeafHeight(
|
|
displayLeaf,
|
|
st.displayMath.fallbackStyle,
|
|
block->textWidth);
|
|
formulaHeight = fallbackPadding.top()
|
|
+ textHeight
|
|
+ fallbackPadding.bottom();
|
|
block->textRect.setSize(QSize(block->textWidth, textHeight));
|
|
}
|
|
const auto centered = (st.displayMath.align == ::style::al_center)
|
|
&& (formulaWidth <= contentLogicalWidth);
|
|
block->formulaAlign = centered ? ::style::al_center : ::style::al_left;
|
|
block->contentRect = QRect(
|
|
contentLeft,
|
|
contentTop,
|
|
contentWidth,
|
|
formulaHeight);
|
|
block->formulaRect = QRect(
|
|
centered
|
|
? (contentLeft + ((contentLogicalWidth - 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 = (block->formulaRect.width()
|
|
> block->visibleFormulaRect.width());
|
|
block->horizontalScrollMax = scrollOwner
|
|
? std::max(block->formulaRect.width() - block->contentRect.width(), 0)
|
|
: 0;
|
|
if (scrollOwner) {
|
|
block->scrollViewportRect = block->contentRect;
|
|
block->scrollLogicalContentRect = block->formulaRect;
|
|
if (block->horizontalScrollMax > 0) {
|
|
block->scrollScrollbarTrackRect = QRect(
|
|
block->contentRect.x(),
|
|
block->contentRect.y()
|
|
+ block->contentRect.height()
|
|
+ st.table.scrollbarSkip,
|
|
block->contentRect.width(),
|
|
st.table.scrollbarHeight);
|
|
block->outer.setHeight(
|
|
block->outer.height()
|
|
+ ScrollbarReserveHeight(
|
|
scrollOwner,
|
|
block->horizontalScrollMax,
|
|
st));
|
|
}
|
|
}
|
|
block->outer.setHeight(ResolveEditableHeight(
|
|
block->outer.height(),
|
|
context));
|
|
if (!(formula && formula->measured.success)) {
|
|
const auto &fallbackPadding = st.displayMath.fallbackPadding;
|
|
const auto &displayLeaf = usePlaceholder
|
|
? block->placeholderLeaf
|
|
: block->fallbackLeaf;
|
|
block->textRect.moveTo(
|
|
block->formulaRect.x() + fallbackPadding.left(),
|
|
block->formulaRect.y() + fallbackPadding.top());
|
|
block->firstLineBaseline = LeafFirstLineBaseline(
|
|
displayLeaf,
|
|
block->textRect,
|
|
st.displayMath.fallbackStyle);
|
|
}
|
|
FinishBlockGeometry(block);
|
|
return block->outer.y() + block->outer.height();
|
|
}
|
|
|
|
[[nodiscard]] std::optional<int> LayoutTableBlockGeometry(
|
|
const PreparedBlock &prepared,
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
bool scrollOwner,
|
|
LayoutContext context) {
|
|
if (!block || prepared.tableRows.size() != block->tableRows.size()) {
|
|
return std::nullopt;
|
|
}
|
|
const auto columnCount = prepared.tableColumnCount;
|
|
for (auto rowIndex = 0, rowCount = int(prepared.tableRows.size());
|
|
rowIndex != rowCount;
|
|
++rowIndex) {
|
|
const auto &preparedRow = prepared.tableRows[rowIndex];
|
|
const auto &row = block->tableRows[rowIndex];
|
|
if (preparedRow.cells.size() != row.cells.size()) {
|
|
return std::nullopt;
|
|
}
|
|
for (auto cellIndex = 0, cellCount = int(preparedRow.cells.size());
|
|
cellIndex != cellCount;
|
|
++cellIndex) {
|
|
const auto &preparedCell = preparedRow.cells[cellIndex];
|
|
const auto &cell = row.cells[cellIndex];
|
|
const auto usePlaceholder = preparedCell.text.text.isEmpty()
|
|
&& !preparedCell.editPlaceholderText.isEmpty();
|
|
if (preparedCell.header != cell.header
|
|
|| std::max(preparedCell.column, 0) != cell.column
|
|
|| std::max(preparedCell.colspan, 1) != cell.colspan
|
|
|| std::max(preparedCell.rowspan, 1) != cell.rowspan
|
|
|| preparedCell.verticalAlignment != cell.verticalAlignment
|
|
|| MissingRetainedLeaf(preparedCell.text.text, cell.leaf)
|
|
|| MissingRetainedPlaceholderLeaf(
|
|
usePlaceholder,
|
|
cell.placeholderLeaf)) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
}
|
|
const auto useTitlePlaceholder = prepared.text.text.isEmpty()
|
|
&& !prepared.editPlaceholderText.isEmpty();
|
|
const auto hasTitle = !prepared.text.text.isEmpty()
|
|
|| prepared.forceTextSegment;
|
|
if ((hasTitle && MissingRetainedLeaf(prepared.text.text, block->leaf))
|
|
|| MissingRetainedPlaceholderLeaf(
|
|
useTitlePlaceholder,
|
|
block->placeholderLeaf)) {
|
|
return std::nullopt;
|
|
}
|
|
ClearBlockGeometry(block);
|
|
for (auto &row : block->tableRows) {
|
|
ResetTableRowGeometry(&row);
|
|
}
|
|
auto tableTop = top;
|
|
if (hasTitle) {
|
|
const auto &displayLeaf = useTitlePlaceholder
|
|
? block->placeholderLeaf
|
|
: block->leaf;
|
|
block->textWidth = std::max(width, 1);
|
|
block->textRect = QRect(
|
|
left,
|
|
top,
|
|
block->textWidth,
|
|
ResolveEditableHeight(
|
|
std::max(
|
|
displayLeaf.countHeight(block->textWidth, true),
|
|
TextLineHeight(st.body)),
|
|
context));
|
|
block->firstLineBaseline = LeafFirstLineBaseline(
|
|
displayLeaf,
|
|
block->textRect,
|
|
st.body);
|
|
tableTop = block->textRect.y()
|
|
+ block->textRect.height()
|
|
+ st.table.captionSkip;
|
|
}
|
|
const auto visibleWidth = std::max(width, 1);
|
|
const auto layoutWidth = std::max(
|
|
(logicalWidth > 0) ? logicalWidth : visibleWidth,
|
|
1);
|
|
if (!columnCount || prepared.tableRows.empty()) {
|
|
if (!block->textRect.isEmpty()) {
|
|
block->contentRect = block->textRect;
|
|
block->outer = block->textRect;
|
|
} else {
|
|
block->outer = QRect(left, top, visibleWidth, 0);
|
|
}
|
|
FinishBlockGeometry(block);
|
|
return block->outer.y() + block->outer.height();
|
|
}
|
|
auto rows = std::vector<TableRowGeometryData>();
|
|
rows.reserve(prepared.tableRows.size());
|
|
for (auto rowIndex = 0, rowCount = int(prepared.tableRows.size());
|
|
rowIndex != rowCount;
|
|
++rowIndex) {
|
|
const auto &preparedRow = prepared.tableRows[rowIndex];
|
|
auto row = TableRowGeometryData();
|
|
row.header = preparedRow.header;
|
|
row.cells.reserve(preparedRow.cells.size());
|
|
for (auto cellIndex = 0, cellCount = int(preparedRow.cells.size());
|
|
cellIndex != cellCount;
|
|
++cellIndex) {
|
|
auto &cell = block->tableRows[rowIndex].cells[cellIndex];
|
|
const auto &preparedCell = preparedRow.cells[cellIndex];
|
|
const auto usePlaceholder = preparedCell.text.text.isEmpty()
|
|
&& !preparedCell.editPlaceholderText.isEmpty();
|
|
const auto &displayLeaf = usePlaceholder
|
|
? cell.placeholderLeaf
|
|
: cell.leaf;
|
|
const auto &textStyle = TableCellTextStyle(cell, st);
|
|
auto cellData = TableCellGeometryData();
|
|
cellData.cell = &cell;
|
|
cellData.usePlaceholder = usePlaceholder;
|
|
cellData.minimumWidth = LeafMinimumWidth(displayLeaf);
|
|
cellData.preferredWidth = displayLeaf.maxWidth();
|
|
if (cell.cachedPreferredWidth != cellData.preferredWidth) {
|
|
cell.cachedPreferredWidth = cellData.preferredWidth;
|
|
cell.cachedPreferredHeight = std::max(
|
|
displayLeaf.countHeight(
|
|
std::max(cellData.preferredWidth, 1),
|
|
true),
|
|
TextLineHeight(textStyle));
|
|
}
|
|
cellData.preferredHeight = cell.cachedPreferredHeight;
|
|
row.cells.push_back(std::move(cellData));
|
|
}
|
|
rows.push_back(std::move(row));
|
|
}
|
|
block->tableColumnWidths = ComputeTableColumnWidths(
|
|
rows,
|
|
columnCount,
|
|
layoutWidth,
|
|
st,
|
|
block->tableBordered,
|
|
&block->overflowed);
|
|
const auto &padding = st.table.cellPadding;
|
|
const auto border = TableBorder(block->tableBordered, st);
|
|
auto tableWidth = border;
|
|
for (const auto columnWidth : block->tableColumnWidths) {
|
|
tableWidth += columnWidth + border;
|
|
}
|
|
auto columnLefts = std::vector<int>(columnCount, left + border);
|
|
auto x = left + border;
|
|
for (auto column = 0; column != columnCount; ++column) {
|
|
columnLefts[column] = x;
|
|
x += block->tableColumnWidths[column] + border;
|
|
}
|
|
auto rowHeights = std::vector<int>(rows.size(), 0);
|
|
auto rowSpans = std::vector<TableSpannedCellGeometryData>();
|
|
for (auto rowIndex = 0, rowCount = int(rows.size()); rowIndex != rowCount; ++rowIndex) {
|
|
for (auto &cellData : rows[rowIndex].cells) {
|
|
if (!cellData.cell) {
|
|
continue;
|
|
}
|
|
const auto spanWidth = TableSpanWidth(
|
|
block->tableColumnWidths,
|
|
cellData.cell->column,
|
|
cellData.cell->colspan,
|
|
border);
|
|
cellData.cell->textWidth = std::max(
|
|
spanWidth - padding.left() - padding.right(),
|
|
1);
|
|
const auto &textStyle = TableCellTextStyle(*cellData.cell, st);
|
|
const auto &displayLeaf = cellData.usePlaceholder
|
|
? cellData.cell->placeholderLeaf
|
|
: cellData.cell->leaf;
|
|
const auto naturalTextHeight = (cellData.cell->textWidth
|
|
>= cellData.preferredWidth)
|
|
? cellData.preferredHeight
|
|
: std::max(
|
|
displayLeaf.countHeight(
|
|
cellData.cell->textWidth,
|
|
true),
|
|
TextLineHeight(textStyle));
|
|
cellData.textHeight = ResolveEditableHeight(
|
|
naturalTextHeight,
|
|
context);
|
|
const auto outerHeight = cellData.textHeight
|
|
+ padding.top()
|
|
+ padding.bottom();
|
|
if (cellData.cell->rowspan == 1) {
|
|
rowHeights[rowIndex] = std::max(rowHeights[rowIndex], outerHeight);
|
|
} else {
|
|
rowSpans.push_back({ rowIndex, &cellData });
|
|
}
|
|
}
|
|
}
|
|
std::sort(
|
|
rowSpans.begin(),
|
|
rowSpans.end(),
|
|
[](const TableSpannedCellGeometryData &a,
|
|
const TableSpannedCellGeometryData &b) {
|
|
const auto aSpan = a.cell ? a.cell->cell->rowspan : 0;
|
|
const auto bSpan = b.cell ? b.cell->cell->rowspan : 0;
|
|
return (aSpan < bSpan)
|
|
|| ((aSpan == bSpan) && (a.row < b.row))
|
|
|| ((aSpan == bSpan)
|
|
&& (a.row == b.row)
|
|
&& a.cell
|
|
&& b.cell
|
|
&& (a.cell->cell->column < b.cell->cell->column));
|
|
});
|
|
for (const auto &spanned : rowSpans) {
|
|
if (!spanned.cell || !spanned.cell->cell) {
|
|
continue;
|
|
}
|
|
const auto outerHeight = spanned.cell->textHeight
|
|
+ padding.top()
|
|
+ padding.bottom();
|
|
const auto currentHeight = TableSpanHeight(
|
|
rowHeights,
|
|
spanned.row,
|
|
spanned.cell->cell->rowspan,
|
|
border);
|
|
DistributeSpanDelta(
|
|
&rowHeights,
|
|
spanned.row,
|
|
spanned.row + spanned.cell->cell->rowspan,
|
|
std::max(outerHeight - currentHeight, 0));
|
|
}
|
|
auto y = tableTop + border;
|
|
for (auto rowIndex = 0, rowCount = int(rows.size()); rowIndex != rowCount; ++rowIndex) {
|
|
auto &rowData = rows[rowIndex];
|
|
const auto rowHeight = rowHeights[rowIndex];
|
|
auto &row = block->tableRows[rowIndex];
|
|
row.header = rowData.header;
|
|
row.logicalOuter = QRect(
|
|
left + border,
|
|
y,
|
|
std::max(tableWidth - (2 * border), 0),
|
|
rowHeight);
|
|
row.outer = row.logicalOuter;
|
|
for (auto cellIndex = 0, cellCount = int(rowData.cells.size());
|
|
cellIndex != cellCount;
|
|
++cellIndex) {
|
|
auto &cellData = rowData.cells[cellIndex];
|
|
if (!cellData.cell) {
|
|
continue;
|
|
}
|
|
auto &cell = *cellData.cell;
|
|
const auto column = std::clamp(cell.column, 0, columnCount - 1);
|
|
const auto spanWidth = TableSpanWidth(
|
|
block->tableColumnWidths,
|
|
cell.column,
|
|
cell.colspan,
|
|
border);
|
|
const auto spanHeight = TableSpanHeight(
|
|
rowHeights,
|
|
rowIndex,
|
|
cell.rowspan,
|
|
border);
|
|
const auto cellTop = y;
|
|
const auto contentHeight = std::max(
|
|
spanHeight - padding.top() - padding.bottom(),
|
|
0);
|
|
auto textTop = cellTop + padding.top();
|
|
switch (cell.verticalAlignment) {
|
|
case PreparedTableCellVerticalAlignment::Middle:
|
|
textTop += std::max((contentHeight - cellData.textHeight) / 2, 0);
|
|
break;
|
|
case PreparedTableCellVerticalAlignment::Bottom:
|
|
textTop = cellTop
|
|
+ spanHeight
|
|
- padding.bottom()
|
|
- cellData.textHeight;
|
|
break;
|
|
case PreparedTableCellVerticalAlignment::Top:
|
|
break;
|
|
}
|
|
cell.logicalOuter = QRect(
|
|
columnLefts[column],
|
|
cellTop,
|
|
spanWidth,
|
|
spanHeight);
|
|
cell.logicalTextRect = QRect(
|
|
columnLefts[column] + padding.left(),
|
|
textTop,
|
|
cell.textWidth,
|
|
cellData.textHeight);
|
|
cell.outer = cell.logicalOuter;
|
|
cell.textRect = cell.logicalTextRect;
|
|
}
|
|
y += rowHeight + border;
|
|
}
|
|
const auto tableHeight = std::max(y - tableTop, border);
|
|
block->tableRect = QRect(left, tableTop, tableWidth, tableHeight);
|
|
block->visibleTableRect = QRect(
|
|
left,
|
|
tableTop,
|
|
std::min(tableWidth, visibleWidth),
|
|
tableHeight);
|
|
block->overflowed = (block->tableRect.width()
|
|
> block->visibleTableRect.width());
|
|
block->horizontalScrollMax = scrollOwner
|
|
? std::max(
|
|
block->tableRect.width() - block->visibleTableRect.width(),
|
|
0)
|
|
: 0;
|
|
auto tableContentRect = block->visibleTableRect;
|
|
if (scrollOwner) {
|
|
block->scrollLogicalContentRect = block->tableRect;
|
|
block->scrollViewportRect = block->visibleTableRect;
|
|
if (block->horizontalScrollMax > 0) {
|
|
block->scrollScrollbarTrackRect = QRect(
|
|
block->visibleTableRect.x(),
|
|
block->tableRect.y()
|
|
+ block->tableRect.height()
|
|
+ st.table.scrollbarSkip,
|
|
block->visibleTableRect.width(),
|
|
st.table.scrollbarHeight);
|
|
}
|
|
}
|
|
if (block->horizontalScrollMax > 0) {
|
|
block->tableScrollbarTrackRect = QRect(
|
|
block->visibleTableRect.x(),
|
|
block->tableRect.y()
|
|
+ block->tableRect.height()
|
|
+ st.table.scrollbarSkip,
|
|
block->visibleTableRect.width(),
|
|
st.table.scrollbarHeight);
|
|
block->tableScrollbarThumbRect = QRect();
|
|
block->scrollScrollbarThumbRect = block->tableScrollbarThumbRect;
|
|
tableContentRect = tableContentRect.united(block->tableScrollbarTrackRect);
|
|
}
|
|
block->contentRect = block->textRect.isEmpty()
|
|
? tableContentRect
|
|
: tableContentRect.isEmpty()
|
|
? block->textRect
|
|
: block->textRect.united(tableContentRect);
|
|
block->outer = block->contentRect;
|
|
if (!block->textRect.isEmpty()) {
|
|
FinishBlockGeometry(block);
|
|
return block->outer.y() + block->outer.height();
|
|
}
|
|
for (auto rowIndex = 0, rowCount = int(prepared.tableRows.size());
|
|
rowIndex != rowCount;
|
|
++rowIndex) {
|
|
for (auto cellIndex = 0, cellCount = int(prepared.tableRows[rowIndex].cells.size());
|
|
cellIndex != cellCount;
|
|
++cellIndex) {
|
|
auto &cell = block->tableRows[rowIndex].cells[cellIndex];
|
|
const auto &preparedCell = prepared.tableRows[rowIndex].cells[cellIndex];
|
|
const auto usePlaceholder = preparedCell.text.text.isEmpty()
|
|
&& !preparedCell.editPlaceholderText.isEmpty();
|
|
const auto &displayLeaf = usePlaceholder
|
|
? cell.placeholderLeaf
|
|
: cell.leaf;
|
|
if (displayLeaf.isEmpty()) {
|
|
continue;
|
|
}
|
|
block->firstLineBaseline = LeafFirstLineBaseline(
|
|
displayLeaf,
|
|
cell.textRect,
|
|
TableCellTextStyle(cell, st));
|
|
FinishBlockGeometry(block);
|
|
return block->outer.y() + block->outer.height();
|
|
}
|
|
}
|
|
FinishBlockGeometry(block);
|
|
return block->outer.y() + block->outer.height();
|
|
}
|
|
|
|
[[nodiscard]] std::optional<int> LayoutPlaceholderBlockGeometry(
|
|
const PreparedBlock &prepared,
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
LayoutContext context) {
|
|
if (!block
|
|
|| MissingRetainedLeaf(prepared.placeholder.label, block->labelLeaf)) {
|
|
return std::nullopt;
|
|
}
|
|
ClearBlockGeometry(block);
|
|
const auto &style = st.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;
|
|
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;
|
|
if (block->placeholderRuntime
|
|
&& block->placeholderRuntime->ripple
|
|
&& block->placeholderRuntime->rippleSize != block->mediaRect.size()) {
|
|
block->placeholderRuntime->ripple = nullptr;
|
|
block->placeholderRuntime->rippleSize = QSize();
|
|
}
|
|
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;
|
|
if (!LayoutMediaCaptionGeometry(
|
|
block,
|
|
prepared,
|
|
st,
|
|
contentLeft,
|
|
bottom,
|
|
contentWidth,
|
|
style.captionSkip,
|
|
&bottom,
|
|
context)) {
|
|
return std::nullopt;
|
|
}
|
|
block->contentRect = QRect(
|
|
left,
|
|
top,
|
|
blockWidth,
|
|
std::max(bottom - top, mediaHeight));
|
|
block->outer = block->contentRect;
|
|
FinishBlockGeometry(block);
|
|
return block->outer.y() + block->outer.height();
|
|
}
|
|
|
|
[[nodiscard]] std::optional<int> LayoutRelatedArticleBlockGeometry(
|
|
const PreparedBlock &prepared,
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
LayoutContext context) {
|
|
if (!block
|
|
|| MissingRetainedLeaf(prepared.relatedArticle.title, block->labelLeaf)
|
|
|| MissingRetainedLeaf(
|
|
prepared.relatedArticle.description,
|
|
block->subtitleLeaf)
|
|
|| MissingRetainedLeaf(prepared.relatedArticle.footer, block->actionLeaf)) {
|
|
return std::nullopt;
|
|
}
|
|
ClearBlockGeometry(block);
|
|
const auto &card = st.relatedArticle;
|
|
const auto blockWidth = std::max(width, 1);
|
|
const auto hasThumbnail = (prepared.relatedArticle.photoId != 0);
|
|
const auto thumbnailSize = hasThumbnail
|
|
? std::max(card.thumbnailSize, 1)
|
|
: 0;
|
|
const auto thumbnailSkip = hasThumbnail ? card.thumbnailSkip : 0;
|
|
const auto contentLeft = left + card.padding.left();
|
|
const auto contentWidth = std::max(
|
|
blockWidth
|
|
- card.padding.left()
|
|
- card.padding.right()
|
|
- thumbnailSize
|
|
- thumbnailSkip,
|
|
1);
|
|
auto titleHeight = 0;
|
|
if (!prepared.relatedArticle.title.isEmpty()) {
|
|
block->labelWidth = contentWidth;
|
|
titleHeight = LeafHeightWithLineLimit(
|
|
block->labelLeaf,
|
|
card.titleStyle,
|
|
block->labelWidth,
|
|
card.titleLines);
|
|
}
|
|
auto subtitleHeight = 0;
|
|
if (!prepared.relatedArticle.description.isEmpty()) {
|
|
block->subtitleWidth = contentWidth;
|
|
subtitleHeight = LeafHeightWithLineLimit(
|
|
block->subtitleLeaf,
|
|
card.subtitleStyle,
|
|
block->subtitleWidth,
|
|
card.subtitleLines);
|
|
}
|
|
auto footerHeight = 0;
|
|
if (!prepared.relatedArticle.footer.isEmpty()) {
|
|
block->actionWidth = contentWidth;
|
|
footerHeight = LeafHeightWithLineLimit(
|
|
block->actionLeaf,
|
|
card.footerStyle,
|
|
block->actionWidth,
|
|
card.footerLines);
|
|
}
|
|
auto textHeight = 0;
|
|
if (titleHeight) {
|
|
textHeight += titleHeight;
|
|
}
|
|
if (subtitleHeight) {
|
|
textHeight += subtitleHeight + (textHeight ? card.textSkip : 0);
|
|
}
|
|
if (footerHeight) {
|
|
textHeight += footerHeight + (textHeight ? card.footerSkip : 0);
|
|
}
|
|
const auto cardContentHeight = std::max(textHeight, thumbnailSize);
|
|
const auto cardHeight = card.padding.top()
|
|
+ cardContentHeight
|
|
+ card.padding.bottom()
|
|
+ card.separator;
|
|
block->mediaRect = QRect(left, top, blockWidth, cardHeight);
|
|
block->visibleMediaRect = block->mediaRect;
|
|
if (hasThumbnail) {
|
|
block->thumbnailRect = QRect(
|
|
left + blockWidth - card.padding.right() - thumbnailSize,
|
|
top + card.padding.top()
|
|
+ std::max((cardContentHeight - thumbnailSize) / 2, 0),
|
|
thumbnailSize,
|
|
thumbnailSize);
|
|
}
|
|
auto textTop = top + card.padding.top()
|
|
+ std::max((cardContentHeight - textHeight) / 2, 0);
|
|
if (titleHeight) {
|
|
block->labelRect = QRect(
|
|
contentLeft,
|
|
textTop,
|
|
block->labelWidth,
|
|
titleHeight);
|
|
textTop += titleHeight;
|
|
}
|
|
if (subtitleHeight) {
|
|
textTop += block->labelRect.isEmpty() ? 0 : card.textSkip;
|
|
block->subtitleRect = QRect(
|
|
contentLeft,
|
|
textTop,
|
|
block->subtitleWidth,
|
|
subtitleHeight);
|
|
textTop += subtitleHeight;
|
|
}
|
|
if (footerHeight) {
|
|
textTop += (block->labelRect.isEmpty() && block->subtitleRect.isEmpty())
|
|
? 0
|
|
: card.footerSkip;
|
|
block->actionRect = QRect(
|
|
contentLeft,
|
|
textTop,
|
|
block->actionWidth,
|
|
footerHeight);
|
|
}
|
|
const auto setBaseline = [&](const Ui::Text::String &leaf,
|
|
QRect rect,
|
|
const style::TextStyle &textStyle) {
|
|
if (rect.isEmpty() || leaf.isEmpty()) {
|
|
return false;
|
|
}
|
|
block->firstLineBaseline = LeafFirstLineBaseline(leaf, rect, textStyle);
|
|
return true;
|
|
};
|
|
if (!setBaseline(block->labelLeaf, block->labelRect, card.titleStyle)
|
|
&& !setBaseline(
|
|
block->subtitleLeaf,
|
|
block->subtitleRect,
|
|
card.subtitleStyle)
|
|
&& !setBaseline(
|
|
block->actionLeaf,
|
|
block->actionRect,
|
|
card.footerStyle)) {
|
|
block->firstLineBaseline = top + card.padding.top();
|
|
}
|
|
block->contentRect = block->mediaRect;
|
|
block->outer = block->mediaRect;
|
|
FinishBlockGeometry(block);
|
|
return block->outer.y() + block->outer.height();
|
|
}
|
|
|
|
[[nodiscard]] std::optional<int> LayoutFramedMediaBlockGeometry(
|
|
LaidOutBlock *block,
|
|
const PreparedBlock &prepared,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
QMargins padding,
|
|
int captionSkip,
|
|
bool limitWidth,
|
|
bool centerMedia,
|
|
int intrinsicWidth,
|
|
LayoutContext context) {
|
|
if (!block) {
|
|
return std::nullopt;
|
|
}
|
|
ClearBlockGeometry(block);
|
|
const auto blockWidth = std::max(width, 1);
|
|
const auto availableLeft = left + padding.left();
|
|
const auto mediaTop = top + padding.top();
|
|
const auto availableWidth = std::max(
|
|
blockWidth - padding.left() - padding.right(),
|
|
1);
|
|
const auto mediaWidth = limitWidth
|
|
? LimitedMediaWidth(availableWidth, intrinsicWidth)
|
|
: availableWidth;
|
|
const auto mediaLeft = centerMedia
|
|
? (availableLeft + std::max((availableWidth - mediaWidth) / 2, 0))
|
|
: availableLeft;
|
|
const auto mediaHeight = block->mediaBlock
|
|
? block->mediaBlock->resizeGetHeight(mediaWidth)
|
|
: 0;
|
|
block->mediaRect = QRect(mediaLeft, mediaTop, mediaWidth, mediaHeight);
|
|
block->visibleMediaRect = block->mediaRect;
|
|
if (block->mediaBlock) {
|
|
ApplyMediaBlockGeometry(
|
|
block,
|
|
block->mediaRect,
|
|
st,
|
|
context.mediaPixelScale);
|
|
}
|
|
auto bottom = block->mediaRect.y() + block->mediaRect.height()
|
|
+ padding.bottom();
|
|
if (!LayoutMediaCaptionGeometry(
|
|
block,
|
|
prepared,
|
|
st,
|
|
block->mediaRect.x(),
|
|
bottom,
|
|
std::max(block->mediaRect.width(), 1),
|
|
captionSkip,
|
|
&bottom,
|
|
context)) {
|
|
return std::nullopt;
|
|
}
|
|
block->contentRect = QRect(
|
|
block->mediaRect.x(),
|
|
block->mediaRect.y(),
|
|
block->mediaRect.width(),
|
|
std::max(bottom - block->mediaRect.y(), block->mediaRect.height()));
|
|
block->outer = QRect(
|
|
left,
|
|
top,
|
|
blockWidth,
|
|
std::max(bottom - top, block->mediaRect.height()));
|
|
FinishBlockGeometry(block);
|
|
return block->outer.y() + block->outer.height();
|
|
}
|
|
|
|
[[nodiscard]] std::optional<int> LayoutCardMediaBlockGeometry(
|
|
LaidOutBlock *block,
|
|
const PreparedBlock &prepared,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
QMargins padding,
|
|
int captionSkip,
|
|
LayoutContext context) {
|
|
if (!block) {
|
|
return std::nullopt;
|
|
}
|
|
ClearBlockGeometry(block);
|
|
const auto blockWidth = std::max(width, 1);
|
|
const auto cardHeight = block->mediaBlock
|
|
? block->mediaBlock->resizeGetHeight(blockWidth)
|
|
: 0;
|
|
block->mediaRect = QRect(left, top, blockWidth, cardHeight);
|
|
block->visibleMediaRect = block->mediaRect;
|
|
if (block->mediaBlock) {
|
|
ApplyMediaBlockGeometry(
|
|
block,
|
|
block->mediaRect,
|
|
st,
|
|
context.mediaPixelScale);
|
|
}
|
|
auto bottom = top + cardHeight;
|
|
if (!LayoutMediaCaptionGeometry(
|
|
block,
|
|
prepared,
|
|
st,
|
|
left + padding.left(),
|
|
bottom,
|
|
std::max(blockWidth - padding.left() - padding.right(), 1),
|
|
captionSkip,
|
|
&bottom,
|
|
context)) {
|
|
return std::nullopt;
|
|
}
|
|
block->contentRect = QRect(
|
|
left,
|
|
top,
|
|
blockWidth,
|
|
std::max(bottom - top, cardHeight));
|
|
block->outer = block->contentRect;
|
|
FinishBlockGeometry(block);
|
|
return block->outer.y() + block->outer.height();
|
|
}
|
|
|
|
std::optional<int> RecountSimpleLaidOutBlock(
|
|
const PreparedBlock &prepared,
|
|
const std::vector<PreparedFormulaSlot> &formulas,
|
|
LaidOutBlock *block,
|
|
const style::Markdown &st,
|
|
int left,
|
|
int top,
|
|
int width,
|
|
int logicalWidth,
|
|
bool scrollOwner,
|
|
LayoutContext context) {
|
|
switch (prepared.kind) {
|
|
case PreparedBlockKind::Paragraph:
|
|
case PreparedBlockKind::Thinking:
|
|
case PreparedBlockKind::Heading:
|
|
return LayoutFlowBlockGeometry(
|
|
prepared,
|
|
block,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
logicalWidth,
|
|
scrollOwner,
|
|
context);
|
|
case PreparedBlockKind::CodeBlock:
|
|
return LayoutCodeBlockGeometry(
|
|
prepared,
|
|
block,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
logicalWidth,
|
|
scrollOwner,
|
|
context);
|
|
case PreparedBlockKind::Rule:
|
|
return LayoutRuleBlockGeometry(block, st, left, top, width);
|
|
case PreparedBlockKind::DisplayMath:
|
|
return LayoutDisplayMathBlockGeometry(
|
|
prepared,
|
|
formulas,
|
|
block,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
logicalWidth,
|
|
scrollOwner,
|
|
context);
|
|
case PreparedBlockKind::Table:
|
|
return LayoutTableBlockGeometry(
|
|
prepared,
|
|
block,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
logicalWidth,
|
|
scrollOwner,
|
|
context);
|
|
case PreparedBlockKind::Photo:
|
|
if (!block
|
|
|| !block->mediaBlock
|
|
|| !block->mediaBlock->alive()) {
|
|
return std::nullopt;
|
|
}
|
|
return LayoutFramedMediaBlockGeometry(
|
|
block,
|
|
prepared,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
st.photo.padding,
|
|
st.photo.captionSkip,
|
|
true,
|
|
true,
|
|
prepared.photo.width,
|
|
context);
|
|
case PreparedBlockKind::Video:
|
|
if (!block
|
|
|| !block->mediaBlock
|
|
|| !block->mediaBlock->alive()) {
|
|
return std::nullopt;
|
|
}
|
|
return LayoutFramedMediaBlockGeometry(
|
|
block,
|
|
prepared,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
st.photo.padding,
|
|
st.photo.captionSkip,
|
|
true,
|
|
true,
|
|
prepared.video.media.width,
|
|
context);
|
|
case PreparedBlockKind::Audio:
|
|
if (!block
|
|
|| !block->mediaBlock
|
|
|| !block->mediaBlock->alive()) {
|
|
return std::nullopt;
|
|
}
|
|
return LayoutCardMediaBlockGeometry(
|
|
block,
|
|
prepared,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
st.audio.padding,
|
|
st.audio.captionSkip,
|
|
context);
|
|
case PreparedBlockKind::Map:
|
|
if (!block
|
|
|| !block->mediaBlock
|
|
|| !block->mediaBlock->alive()) {
|
|
return std::nullopt;
|
|
}
|
|
return LayoutFramedMediaBlockGeometry(
|
|
block,
|
|
prepared,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
st.photo.padding,
|
|
st.photo.captionSkip,
|
|
false,
|
|
false,
|
|
0,
|
|
context);
|
|
case PreparedBlockKind::Channel:
|
|
if (!block
|
|
|| !block->mediaBlock
|
|
|| !block->mediaBlock->alive()) {
|
|
return std::nullopt;
|
|
}
|
|
return LayoutCardMediaBlockGeometry(
|
|
block,
|
|
prepared,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
st.channel.padding,
|
|
st.audio.captionSkip,
|
|
context);
|
|
case PreparedBlockKind::GroupedMedia:
|
|
if (!block
|
|
|| !block->mediaBlock
|
|
|| !block->mediaBlock->alive()) {
|
|
return std::nullopt;
|
|
}
|
|
return LayoutFramedMediaBlockGeometry(
|
|
block,
|
|
prepared,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
st.groupedMedia.padding,
|
|
st.groupedMedia.captionSkip,
|
|
false,
|
|
false,
|
|
0,
|
|
context);
|
|
case PreparedBlockKind::RelatedArticle:
|
|
return LayoutRelatedArticleBlockGeometry(
|
|
prepared,
|
|
block,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
context);
|
|
case PreparedBlockKind::Placeholder:
|
|
return LayoutPlaceholderBlockGeometry(
|
|
prepared,
|
|
block,
|
|
st,
|
|
left,
|
|
top,
|
|
width,
|
|
context);
|
|
case PreparedBlockKind::List:
|
|
case PreparedBlockKind::ListItem:
|
|
case PreparedBlockKind::Quote:
|
|
case PreparedBlockKind::Details:
|
|
case PreparedBlockKind::EmbedPost:
|
|
break;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
} // namespace Iv::Markdown
|