Files
AyuGramDesktop/Telegram/SourceFiles/iv/markdown/iv_markdown_article_selection.cpp
T
2026-07-02 17:49:01 +04:00

1347 lines
39 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_selection.h"
#include "iv/markdown/iv_markdown_prepare_links.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "styles/style_iv.h"
#include <algorithm>
namespace Iv::Markdown {
namespace {
constexpr auto kCodeTabColumns = 4;
const auto kPhotoCopyLabel = u"Photo"_q;
[[nodiscard]] TextForMimeData CopyTextForMathSource(QString source) {
const auto length = int(source.size());
auto result = TextForMimeData::Simple(std::move(source));
if (length > 0) {
result.tags.push_back({
.offset = 0,
.length = length,
.id = Ui::InputField::kTagIvMath,
});
}
return result;
}
struct TableCopySlot {
const LaidOutTableCell *cell = nullptr;
bool origin = false;
};
[[nodiscard]] auto BuildTableCopyGrid(const LaidOutBlock &block)
-> std::vector<std::vector<TableCopySlot>> {
const auto rowCount = int(block.tableRows.size());
const auto columnCount = int(block.tableColumnWidths.size());
auto result = std::vector<std::vector<TableCopySlot>>(
std::max(rowCount, 0),
std::vector<TableCopySlot>(std::max(columnCount, 0)));
for (auto rowIndex = 0; rowIndex != rowCount; ++rowIndex) {
for (const auto &cell : block.tableRows[rowIndex].cells) {
const auto fromRow = std::clamp(rowIndex, 0, rowCount);
const auto toRow = std::clamp(rowIndex + cell.rowspan, 0, rowCount);
const auto fromColumn = std::clamp(cell.column, 0, columnCount);
const auto toColumn = std::clamp(
cell.column + cell.colspan,
0,
columnCount);
for (auto currentRow = fromRow; currentRow != toRow; ++currentRow) {
for (auto currentColumn = fromColumn;
currentColumn != toColumn;
++currentColumn) {
auto &slot = result[currentRow][currentColumn];
slot.cell = &cell;
slot.origin = (currentRow == rowIndex)
&& (currentColumn == cell.column);
}
}
}
}
return result;
}
[[nodiscard]] QRect ClipRectHorizontally(QRect rect, QRect clip) {
if (rect.isEmpty() || clip.isEmpty()) {
return QRect();
}
const auto left = std::max(rect.x(), clip.x());
const auto right = std::min(
rect.x() + rect.width(),
clip.x() + clip.width());
return QRect(
left,
rect.y(),
std::max(right - left, 0),
rect.height());
}
[[nodiscard]] QRect VisibleTextRect(QRect textRect, QRect outerRect) {
return ClipRectHorizontally(textRect, outerRect);
}
void RefreshBlockSegmentRect(
const LaidOutBlock &block,
SelectableSegment *segment) {
if (!segment) {
return;
}
switch (segment->kind) {
case SelectableSegmentKind::DisplayMath:
segment->outerRect = block.visibleFormulaRect;
break;
case SelectableSegmentKind::Table:
segment->outerRect = block.visibleTableRect;
break;
case SelectableSegmentKind::Placeholder:
case SelectableSegmentKind::Photo:
case SelectableSegmentKind::Media:
segment->outerRect = block.visibleMediaRect;
break;
case SelectableSegmentKind::CodeBlock:
segment->outerRect = block.outer;
segment->textRect = VisibleTextRect(
block.textRect,
block.contentRect.isEmpty() ? block.outer : block.contentRect);
break;
case SelectableSegmentKind::TextLeaf: {
auto sourceRect = QRect();
auto containerRect = block.outer;
auto interactionRect = QRect();
if (segment->leaf == &block.leaf) {
sourceRect = block.textRect;
if (block.kind == PreparedBlockKind::Details) {
interactionRect = block.headerRect;
containerRect = block.headerRect;
} else if (block.kind == PreparedBlockKind::CodeBlock) {
containerRect = block.contentRect;
}
} else if (segment->leaf == &block.labelLeaf) {
sourceRect = block.labelRect;
containerRect = block.headerRect.isEmpty()
? block.outer
: block.headerRect;
} else if (segment->leaf == &block.subtitleLeaf) {
sourceRect = block.subtitleRect;
containerRect = block.headerRect.isEmpty()
? block.outer
: block.headerRect;
} else if (segment->leaf == &block.actionLeaf) {
sourceRect = block.actionRect;
}
const auto visibleRect = VisibleTextRect(sourceRect, containerRect);
segment->outerRect = interactionRect.isEmpty()
? visibleRect
: interactionRect;
segment->textRect = visibleRect;
} break;
}
}
[[nodiscard]] TextForMimeData CopyTextForDisplayMath(const LaidOutBlock &block) {
return CopyTextForMathSource(block.copyText);
}
[[nodiscard]] TextForMimeData CopyTextForCodeBlock(
const LaidOutBlock &block,
TextSelection selection = AllTextSelection) {
if (selection == AllTextSelection) {
auto rich = block.codeText;
if (!rich.text.isEmpty()) {
rich.entities.push_back(EntityInText(
EntityType::Pre,
0,
rich.text.size(),
block.codeLanguage));
SortEntities(&rich);
}
return TextForMimeData::Rich(std::move(rich));
}
auto from = 0;
auto to = 0;
auto displayPosition = 0;
auto column = 0;
auto found = false;
const auto &text = block.codeText.text;
for (auto i = 0, count = int(text.size()); i != count; ++i) {
const auto ch = text[i];
const auto width = (ch == QChar::Tabulation)
? (kCodeTabColumns - (column % kCodeTabColumns))
: 1;
const auto nextDisplayPosition = displayPosition + width;
if (selection.to <= displayPosition) {
break;
}
if (selection.from < nextDisplayPosition
&& selection.to > displayPosition) {
if (!found) {
from = i;
found = true;
}
to = i + 1;
}
displayPosition = nextDisplayPosition;
if (Ui::Text::IsNewline(ch)) {
column = 0;
} else {
column += width;
}
}
if (!found || to <= from) {
return TextForMimeData();
}
auto rich = Ui::Text::Mid(block.codeText, from, to - from);
if (!rich.text.isEmpty()) {
rich.entities.push_back(EntityInText(
EntityType::Pre,
0,
rich.text.size(),
block.codeLanguage));
SortEntities(&rich);
}
return TextForMimeData::Rich(std::move(rich));
}
[[nodiscard]] TextForMimeData CopyTextForTable(const LaidOutBlock &block) {
const auto rowCount = int(block.tableRows.size());
const auto columnCount = int(block.tableColumnWidths.size());
auto result = TextForMimeData();
const auto grid = BuildTableCopyGrid(block);
if (!block.leaf.isEmpty()) {
result.append(block.leaf.toTextForMimeData());
if (rowCount > 0 && columnCount > 0) {
result.append(u"\n"_q);
}
}
for (auto rowIndex = 0; rowIndex != rowCount; ++rowIndex) {
if (rowIndex) {
result.append(u"\n"_q);
}
for (auto column = 0; column != columnCount; ++column) {
if (column) {
result.append(u"\t"_q);
}
const auto &slot = grid[rowIndex][column];
if (slot.origin && slot.cell) {
result.append(slot.cell->leaf.toTextForMimeData());
}
}
}
return result;
}
[[nodiscard]] TextForMimeData CopyTextForTableRange(
const LaidOutBlock &block,
int rowFrom,
int rowTill,
int columnFrom,
int columnTill) {
const auto rowCount = int(block.tableRows.size());
const auto columnCount = int(block.tableColumnWidths.size());
rowFrom = std::clamp(rowFrom, 0, rowCount);
rowTill = std::clamp(rowTill, 0, rowCount);
columnFrom = std::clamp(columnFrom, 0, columnCount);
columnTill = std::clamp(columnTill, 0, columnCount);
if (rowTill <= rowFrom || columnTill <= columnFrom) {
return TextForMimeData();
}
const auto grid = BuildTableCopyGrid(block);
auto result = TextForMimeData();
auto emitted = std::vector<const LaidOutTableCell*>();
for (auto rowIndex = rowFrom; rowIndex != rowTill; ++rowIndex) {
if (rowIndex != rowFrom) {
result.append(u"\n"_q);
}
for (auto column = columnFrom; column != columnTill; ++column) {
if (column != columnFrom) {
result.append(u"\t"_q);
}
const auto &slot = grid[rowIndex][column];
if (slot.cell
&& (std::find(
emitted.begin(),
emitted.end(),
slot.cell) == emitted.end())) {
result.append(slot.cell->leaf.toTextForMimeData());
emitted.push_back(slot.cell);
}
}
}
return result;
}
[[nodiscard]] TextForMimeData CopyTextForMediaBlock(
const QString &label,
const Ui::Text::String &captionLeaf) {
auto result = label.isEmpty()
? TextForMimeData()
: TextForMimeData::Simple(label);
if (!captionLeaf.isEmpty()) {
if (!result.empty()) {
result.append(u"\n"_q);
}
result.append(captionLeaf.toTextForMimeData());
}
return result;
}
[[nodiscard]] TextForMimeData CopyTextForSingleMediaBlock(
const LaidOutBlock &block,
const QString &fallback = QString()) {
const auto selectionData = block.mediaBlock
? block.mediaBlock->selectionData()
: MediaBlockSelectionData();
const auto label = !selectionData.copyText.isEmpty()
? selectionData.copyText
: !block.copyText.isEmpty()
? block.copyText
: !block.labelText.isEmpty()
? block.labelText
: fallback;
return CopyTextForMediaBlock(label, block.leaf);
}
[[nodiscard]] TextForMimeData CopyTextForPhotoBlock(const LaidOutBlock &block) {
return CopyTextForSingleMediaBlock(block, kPhotoCopyLabel);
}
[[nodiscard]] TextForMimeData CopyTextForPlaceholderBlock(
const LaidOutBlock &block) {
return CopyTextForMediaBlock(
block.labelText.isEmpty() ? block.copyText : block.labelText,
block.leaf);
}
[[nodiscard]] int AddSelectableSegment(
std::vector<SelectableSegment> *segments,
SelectableSegment segment) {
segment.index = int(segments->size());
segment.length = std::max(segment.length, 0);
segments->push_back(std::move(segment));
return segment.index;
}
[[nodiscard]] int CompareSelectionPositions(
MarkdownArticleSelectionPosition a,
MarkdownArticleSelectionPosition b) {
if (a.segment != b.segment) {
return (a.segment < b.segment) ? -1 : 1;
}
if (a.offset != b.offset) {
return (a.offset < b.offset) ? -1 : 1;
}
return 0;
}
[[nodiscard]] MarkdownArticleSelection NormalizeSelection(
MarkdownArticleSelection selection) {
if (selection.empty()) {
return {};
}
if (CompareSelectionPositions(selection.from, selection.to) > 0) {
std::swap(selection.from, selection.to);
}
return selection;
}
[[nodiscard]] int LastTableCellSegmentIndex(
const std::vector<SelectableSegment> *segments,
int tableSegmentIndex) {
auto result = tableSegmentIndex;
if (!segments) {
return result;
}
for (const auto &segment : *segments) {
if (segment.tableSegmentIndex == tableSegmentIndex) {
result = std::max(result, segment.index);
}
}
return result;
}
[[nodiscard]] std::optional<int> SingleTableCellSelection(
const PaintSelectionState &selectionState,
int tableSegmentIndex) {
if (selectionState.empty()
|| !selectionState.endpoints
|| tableSegmentIndex < 0) {
return std::nullopt;
}
const auto normalized = NormalizeSelection(selectionState.selection);
if (normalized.empty()) {
return std::nullopt;
}
const auto lastCellSegment = LastTableCellSegmentIndex(
selectionState.segments,
tableSegmentIndex);
const auto spansWholeTable = (normalized.from.segment < tableSegmentIndex)
&& (normalized.to.segment > lastCellSegment);
auto tableHit = false;
auto cellSegment = -1;
auto multipleCells = false;
const auto consider = [&](MarkdownArticleSelectionEndpoint endpoint) {
if (!endpoint.valid() || !endpoint.direct) {
return;
}
const auto segment = FindSegment(selectionState.segments, endpoint.segment);
if (!segment) {
return;
}
if (segment->index == tableSegmentIndex) {
tableHit = true;
return;
}
if (segment->tableSegmentIndex != tableSegmentIndex) {
return;
}
if (cellSegment < 0) {
cellSegment = segment->index;
} else if (cellSegment != segment->index) {
multipleCells = true;
}
};
consider(selectionState.endpoints->from);
consider(selectionState.endpoints->to);
if (tableHit || multipleCells || cellSegment < 0 || spansWholeTable) {
return std::nullopt;
}
return cellSegment;
}
[[nodiscard]] std::optional<TextSelection> BaseTextSelectionForSegment(
const SelectableSegment &segment,
MarkdownArticleSelection selection) {
if (selection.empty() || !segment.isTextLeaf()) {
return std::nullopt;
}
selection = NormalizeSelection(selection);
if (selection.empty()
|| selection.from.segment > segment.index
|| selection.to.segment < segment.index) {
return std::nullopt;
}
auto from = 0;
auto to = SegmentLength(segment);
if (selection.from.segment == segment.index) {
from = selection.from.offset;
}
if (selection.to.segment == segment.index) {
to = selection.to.offset;
}
from = std::clamp(from, 0, SegmentLength(segment));
to = std::clamp(to, 0, SegmentLength(segment));
if (from >= to) {
return std::nullopt;
}
return TextSelection(uint16(from), uint16(to));
}
[[nodiscard]] bool RangeSelectsWholeSegment(
const SelectableSegment &segment,
MarkdownArticleSelection selection) {
selection = NormalizeSelection(selection);
if (selection.empty()
|| selection.from.segment > segment.index
|| selection.to.segment < segment.index) {
return false;
}
auto from = 0;
auto to = SegmentLength(segment);
if (selection.from.segment == segment.index) {
from = selection.from.offset;
}
if (selection.to.segment == segment.index) {
to = selection.to.offset;
}
from = std::clamp(from, 0, SegmentLength(segment));
to = std::clamp(to, 0, SegmentLength(segment));
return (from < to);
}
[[nodiscard]] bool IndexInRange(int index, int from, int till) {
return (index >= from) && (index < till);
}
[[nodiscard]] bool RangesIntersect(
int firstFrom,
int firstTill,
int secondFrom,
int secondTill) {
return (firstFrom < secondTill) && (secondFrom < firstTill);
}
[[nodiscard]] const PreparedEditSelection *StructuralSelection(
const PaintSelectionState &selectionState,
PreparedEditSelectionKind kind) {
if (!selectionState.hasStructuralSelection()) {
return nullptr;
}
const auto selection = selectionState.structuralSelection;
return (selection->kind == kind) ? selection : nullptr;
}
[[nodiscard]] bool ContainerHasPrefix(
const PreparedEditBlockContainerPath &path,
const PreparedEditBlockContainerPath &prefix) {
if (path.steps.size() < prefix.steps.size()) {
return false;
}
return std::equal(
prefix.steps.begin(),
prefix.steps.end(),
path.steps.begin());
}
[[nodiscard]] bool PathInBlockRange(
const PreparedEditBlockPath &path,
const PreparedEditBlockRange &range) {
if (path.container == range.container) {
return IndexInRange(path.index, range.from, range.till);
}
if (!ContainerHasPrefix(path.container, range.container)
|| (path.container.steps.size() <= range.container.steps.size())) {
return false;
}
const auto &step = path.container.steps[range.container.steps.size()];
return IndexInRange(step.blockIndex, range.from, range.till);
}
[[nodiscard]] bool PathInListItemRange(
const PreparedEditBlockPath &path,
const PreparedEditListItemRange &range) {
if (!ContainerHasPrefix(path.container, range.block.container)
|| (path.container.steps.size() <= range.block.container.steps.size())) {
return false;
}
const auto &step = path.container.steps[range.block.container.steps.size()];
return (step.kind == PreparedEditBlockContainerKind::ListItemChildren)
&& (step.blockIndex == range.block.index)
&& IndexInRange(step.listItemIndex, range.from, range.till);
}
[[nodiscard]] bool BlockSourceInStructuralSelection(
const PreparedEditBlockSource &source,
const PreparedEditSelection &selection) {
switch (selection.kind) {
case PreparedEditSelectionKind::Blocks:
return PathInBlockRange(source.path, selection.blocks);
case PreparedEditSelectionKind::ListItems:
return PathInListItemRange(source.path, selection.listItems);
case PreparedEditSelectionKind::TableRows:
case PreparedEditSelectionKind::TableCells:
case PreparedEditSelectionKind::None:
return false;
}
return false;
}
[[nodiscard]] bool ListItemSourceInStructuralSelection(
const PreparedEditListItemSource &source,
const PreparedEditSelection &selection) {
switch (selection.kind) {
case PreparedEditSelectionKind::Blocks:
return PathInBlockRange(source.block, selection.blocks);
case PreparedEditSelectionKind::ListItems:
return (source.block == selection.listItems.block)
&& IndexInRange(
source.listItemIndex,
selection.listItems.from,
selection.listItems.till);
case PreparedEditSelectionKind::TableRows:
case PreparedEditSelectionKind::TableCells:
case PreparedEditSelectionKind::None:
return false;
}
return false;
}
[[nodiscard]] bool BlockBelongsToStructuralSelection(
const LaidOutBlock &block,
const PreparedEditSelection &selection) {
return (block.editListItem
&& ListItemSourceInStructuralSelection(*block.editListItem, selection))
|| (block.editBlock
&& BlockSourceInStructuralSelection(*block.editBlock, selection));
}
[[nodiscard]] bool WholeSegmentStructurallySelected(
const SelectableSegment &segment,
const PaintSelectionState &selectionState) {
if (!selectionState.hasStructuralSelection() || !segment.block) {
return false;
}
switch (segment.kind) {
case SelectableSegmentKind::CodeBlock:
case SelectableSegmentKind::DisplayMath:
case SelectableSegmentKind::Table:
case SelectableSegmentKind::Placeholder:
case SelectableSegmentKind::Photo:
case SelectableSegmentKind::Media:
return BlockBelongsToStructuralSelection(
*segment.block,
*selectionState.structuralSelection);
case SelectableSegmentKind::TextLeaf:
return false;
}
return false;
}
[[nodiscard]] std::optional<TextSelection> StructuralTextSelectionForSegment(
const SelectableSegment &segment,
const PaintSelectionState &selectionState) {
if (!selectionState.hasStructuralSelection()
|| !segment.isTextLeaf()
|| !segment.block) {
return std::nullopt;
}
switch (selectionState.structuralSelection->kind) {
case PreparedEditSelectionKind::Blocks:
case PreparedEditSelectionKind::ListItems:
return BlockBelongsToStructuralSelection(
*segment.block,
*selectionState.structuralSelection)
? std::make_optional(AllTextSelection)
: std::nullopt;
case PreparedEditSelectionKind::TableRows:
case PreparedEditSelectionKind::TableCells:
case PreparedEditSelectionKind::None:
return std::nullopt;
}
return std::nullopt;
}
[[nodiscard]] std::optional<TextSelection> ResolveTextSelectionForSegment(
const SelectableSegment &segment,
const PaintSelectionState &selectionState,
bool suppressStructuralSelection) {
if (!selectionState.segments) {
return std::nullopt;
}
if (segment.tableSegmentIndex >= 0) {
if (const auto singleCell = SingleTableCellSelection(
selectionState,
segment.tableSegmentIndex);
singleCell && *singleCell != segment.index) {
return std::nullopt;
}
}
if (segment.tableSegmentIndex >= 0
&& TableSegmentSelected(selectionState, segment.tableSegmentIndex)) {
return std::nullopt;
}
if (const auto structural = StructuralTextSelectionForSegment(
segment,
selectionState)) {
return suppressStructuralSelection ? std::nullopt : structural;
}
return BaseTextSelectionForSegment(segment, selectionState.selection);
}
[[nodiscard]] const LaidOutBlock *FindTableBlock(
const std::vector<SelectableSegment> &segments,
const PreparedEditBlockPath &path) {
for (const auto &segment : segments) {
if (!segment.block
|| (segment.block->kind != PreparedBlockKind::Table)
|| !segment.block->editBlock
|| (segment.block->editBlock->path != path)) {
continue;
}
return segment.block;
}
return nullptr;
}
[[nodiscard]] TextForMimeData TextForStructuralTableSelection(
const std::vector<SelectableSegment> &segments,
const PreparedEditSelection &selection) {
switch (selection.kind) {
case PreparedEditSelectionKind::TableRows: {
const auto block = FindTableBlock(segments, selection.tableRows.block);
return block
? CopyTextForTableRange(
*block,
selection.tableRows.from,
selection.tableRows.till,
0,
int(block->tableColumnWidths.size()))
: TextForMimeData();
}
case PreparedEditSelectionKind::TableCells: {
const auto block = FindTableBlock(segments, selection.tableCells.block);
return block
? CopyTextForTableRange(
*block,
selection.tableCells.rowFrom,
selection.tableCells.rowTill,
selection.tableCells.columnFrom,
selection.tableCells.columnTill)
: TextForMimeData();
}
case PreparedEditSelectionKind::Blocks:
case PreparedEditSelectionKind::ListItems:
case PreparedEditSelectionKind::None:
return TextForMimeData();
}
return TextForMimeData();
}
[[nodiscard]] const style::TextStyle &HeadingTextStyle(
int level,
const style::Markdown &st) {
switch (std::clamp(level, 1, 6)) {
case 1: return st.heading1;
case 2: return st.heading2;
case 3: return st.heading3;
case 4: return st.heading4;
case 5: return st.heading5;
case 6: return st.heading6;
}
return st.heading6;
}
[[nodiscard]] bool IsEmbedPostAuthorSegment(
const SelectableSegment &segment) {
return segment.block
&& (segment.block->kind == PreparedBlockKind::EmbedPost)
&& (segment.leaf == &segment.block->labelLeaf);
}
[[nodiscard]] bool IsEmbedPostDateSegment(
const SelectableSegment &segment) {
return segment.block
&& (segment.block->kind == PreparedBlockKind::EmbedPost)
&& (segment.leaf == &segment.block->subtitleLeaf);
}
} // namespace
void CollectSelectableSegments(
std::vector<LaidOutBlock> *blocks,
std::vector<SelectableSegment> *segments) {
if (!blocks) {
return;
}
for (auto &block : *blocks) {
block.segmentIndex = -1;
block.secondarySegmentIndex = -1;
block.tertiarySegmentIndex = -1;
switch (block.kind) {
case PreparedBlockKind::Paragraph:
case PreparedBlockKind::Thinking:
case PreparedBlockKind::Heading:
case PreparedBlockKind::Details: {
if (block.leaf.isEmpty() && block.textRect.isEmpty()) {
break;
}
auto segment = SelectableSegment();
segment.kind = SelectableSegmentKind::TextLeaf;
segment.leaf = &block.leaf;
segment.block = &block;
segment.outerRect = (block.kind == PreparedBlockKind::Details)
? block.headerRect
: block.textRect;
segment.textRect = block.textRect;
segment.textWidth = block.textWidth;
segment.align = block.flowTextAlign;
segment.length = block.leaf.length();
block.segmentIndex = AddSelectableSegment(
segments,
std::move(segment));
} break;
case PreparedBlockKind::CodeBlock: {
auto segment = SelectableSegment();
segment.kind = SelectableSegmentKind::CodeBlock;
segment.leaf = &block.leaf;
segment.block = &block;
segment.outerRect = block.outer;
segment.textRect = block.textRect;
segment.textWidth = block.textWidth;
segment.length = block.leaf.length();
block.segmentIndex = AddSelectableSegment(
segments,
std::move(segment));
} break;
case PreparedBlockKind::DisplayMath: {
auto segment = SelectableSegment();
segment.kind = SelectableSegmentKind::DisplayMath;
segment.block = &block;
segment.outerRect = block.visibleFormulaRect;
segment.align = block.formulaAlign;
segment.length = 1;
block.segmentIndex = AddSelectableSegment(
segments,
std::move(segment));
} break;
case PreparedBlockKind::Table: {
if (!block.textRect.isEmpty()) {
auto textSegment = SelectableSegment();
textSegment.kind = SelectableSegmentKind::TextLeaf;
textSegment.leaf = &block.leaf;
textSegment.block = &block;
textSegment.outerRect = block.textRect;
textSegment.textRect = block.textRect;
textSegment.textWidth = block.textWidth;
textSegment.align = block.flowTextAlign;
textSegment.length = block.leaf.length();
block.secondarySegmentIndex = AddSelectableSegment(
segments,
std::move(textSegment));
}
auto segment = SelectableSegment();
segment.kind = SelectableSegmentKind::Table;
segment.block = &block;
segment.outerRect = block.visibleTableRect;
segment.length = 1;
block.segmentIndex = AddSelectableSegment(
segments,
std::move(segment));
if (block.secondarySegmentIndex >= 0) {
(*segments)[block.secondarySegmentIndex].parentSegmentIndex
= block.segmentIndex;
}
for (auto &row : block.tableRows) {
for (auto &cell : row.cells) {
auto cellSegment = SelectableSegment();
cellSegment.kind = SelectableSegmentKind::TextLeaf;
cellSegment.leaf = &cell.leaf;
cellSegment.block = &block;
cellSegment.cell = &cell;
cellSegment.outerRect = cell.outer;
cellSegment.textRect = cell.textRect;
cellSegment.textWidth = cell.textWidth;
cellSegment.align = cell.align;
cellSegment.length = cell.leaf.length();
cellSegment.tableSegmentIndex = block.segmentIndex;
cell.tableSegmentIndex = block.segmentIndex;
cell.segmentIndex = AddSelectableSegment(
segments,
std::move(cellSegment));
}
}
} break;
case PreparedBlockKind::RelatedArticle:
case PreparedBlockKind::Placeholder:
case PreparedBlockKind::Photo:
case PreparedBlockKind::Video:
case PreparedBlockKind::Audio:
case PreparedBlockKind::Map:
case PreparedBlockKind::Channel:
case PreparedBlockKind::GroupedMedia: {
auto segment = SelectableSegment();
segment.kind = (block.kind == PreparedBlockKind::Photo)
? SelectableSegmentKind::Photo
: (block.kind == PreparedBlockKind::Placeholder)
? SelectableSegmentKind::Placeholder
: SelectableSegmentKind::Media;
segment.block = &block;
segment.outerRect = block.mediaRect;
segment.length = 1;
block.segmentIndex = AddSelectableSegment(
segments,
std::move(segment));
if (!block.textRect.isEmpty()) {
auto textSegment = SelectableSegment();
textSegment.kind = SelectableSegmentKind::TextLeaf;
textSegment.leaf = &block.leaf;
textSegment.block = &block;
textSegment.outerRect = block.textRect;
textSegment.textRect = block.textRect;
textSegment.textWidth = block.textWidth;
textSegment.length = block.leaf.length();
textSegment.parentSegmentIndex = block.segmentIndex;
block.secondarySegmentIndex = AddSelectableSegment(
segments,
std::move(textSegment));
}
} break;
case PreparedBlockKind::EmbedPost: {
if (!block.labelRect.isEmpty() && !block.labelLeaf.isEmpty()) {
auto authorSegment = SelectableSegment();
authorSegment.kind = SelectableSegmentKind::TextLeaf;
authorSegment.leaf = &block.labelLeaf;
authorSegment.block = &block;
authorSegment.outerRect = block.labelRect;
authorSegment.textRect = block.labelRect;
authorSegment.textWidth = block.labelWidth;
authorSegment.length = block.labelLeaf.length();
block.segmentIndex = AddSelectableSegment(
segments,
std::move(authorSegment));
}
if (!block.subtitleRect.isEmpty() && !block.subtitleLeaf.isEmpty()) {
auto dateSegment = SelectableSegment();
dateSegment.kind = SelectableSegmentKind::TextLeaf;
dateSegment.leaf = &block.subtitleLeaf;
dateSegment.block = &block;
dateSegment.outerRect = block.subtitleRect;
dateSegment.textRect = block.subtitleRect;
dateSegment.textWidth = block.subtitleWidth;
dateSegment.length = block.subtitleLeaf.length();
block.secondarySegmentIndex = AddSelectableSegment(
segments,
std::move(dateSegment));
}
CollectSelectableSegments(&block.children, segments);
if (!block.textRect.isEmpty() && !block.leaf.isEmpty()) {
auto captionSegment = SelectableSegment();
captionSegment.kind = SelectableSegmentKind::TextLeaf;
captionSegment.leaf = &block.leaf;
captionSegment.block = &block;
captionSegment.outerRect = block.textRect;
captionSegment.textRect = block.textRect;
captionSegment.textWidth = block.textWidth;
captionSegment.length = block.leaf.length();
block.tertiarySegmentIndex = AddSelectableSegment(
segments,
std::move(captionSegment));
}
continue;
}
case PreparedBlockKind::List:
case PreparedBlockKind::ListItem:
case PreparedBlockKind::Quote:
case PreparedBlockKind::Rule:
break;
}
CollectSelectableSegments(&block.children, segments);
}
}
void RefreshScrollableSegmentRects(
const std::vector<LaidOutBlock> &blocks,
std::vector<SelectableSegment> *segments) {
if (!segments) {
return;
}
for (const auto &block : blocks) {
RefreshScrollableSegmentRects(block, segments);
}
}
void RefreshScrollableSegmentRects(
const LaidOutBlock &block,
std::vector<SelectableSegment> *segments) {
if (!segments) {
return;
}
const auto refreshIndex = [&](int index) {
if (index < 0 || index >= int(segments->size())) {
return;
}
RefreshBlockSegmentRect(block, &(*segments)[index]);
};
refreshIndex(block.segmentIndex);
refreshIndex(block.secondarySegmentIndex);
refreshIndex(block.tertiarySegmentIndex);
if (block.kind == PreparedBlockKind::Table) {
for (const auto &row : block.tableRows) {
for (const auto &cell : row.cells) {
if (cell.segmentIndex < 0
|| cell.segmentIndex >= int(segments->size())) {
continue;
}
auto &segment = (*segments)[cell.segmentIndex];
segment.outerRect = cell.outer;
segment.textRect = VisibleTextRect(
cell.textRect,
cell.outer);
}
}
}
RefreshScrollableSegmentRects(block.children, segments);
}
void CollectAnchors(
const std::vector<LaidOutBlock> &blocks,
std::vector<std::pair<QString, int>> *anchors) {
if (!anchors) {
return;
}
for (const auto &block : blocks) {
if (!block.anchorId.isEmpty() || !block.anchorIds.empty()) {
const auto top = !block.textRect.isEmpty()
? block.textRect.top()
: [&] {
for (const auto &child : block.children) {
if (child.outer.height() > 0) {
return child.outer.top();
}
}
return block.outer.top();
}();
if (!block.anchorId.isEmpty()) {
anchors->push_back({ block.anchorId, top });
}
for (const auto &anchorId : block.anchorIds) {
anchors->push_back({ anchorId, top });
}
}
CollectAnchors(block.children, anchors);
}
}
const SelectableSegment *FindSegment(
const std::vector<SelectableSegment> *segments,
int index) {
if (!segments || index < 0 || index >= int(segments->size())) {
return nullptr;
}
return &(*segments)[index];
}
int SegmentLength(const SelectableSegment &segment) {
return std::max(segment.length, 0);
}
const style::TextStyle &TextStyleForSegment(
const SelectableSegment &segment,
const style::Markdown &st) {
if (segment.cell) {
return segment.cell->header
? st.table.headerStyle
: st.table.bodyStyle;
} else if (IsEmbedPostAuthorSegment(segment)) {
return st.embedPost.authorStyle;
} else if (IsEmbedPostDateSegment(segment)) {
return st.embedPost.dateStyle;
} else if (segment.kind == SelectableSegmentKind::CodeBlock) {
return st.code;
} else if (segment.block && segment.block->quoteAuthor) {
return st.quoteAuthorStyle;
} else if (!segment.block) {
return st.body;
}
switch (segment.block->kind) {
case PreparedBlockKind::Details:
return st.details.summaryStyle;
case PreparedBlockKind::CodeBlock:
return st.code;
case PreparedBlockKind::Heading:
return HeadingTextStyle(segment.block->headingLevel, st);
default:
return st.body;
}
}
style::color TextColorForSegment(
const SelectableSegment &segment,
const style::Markdown &st) {
if (IsEmbedPostAuthorSegment(segment)) {
return st.embedPost.authorFg;
} else if (IsEmbedPostDateSegment(segment)) {
return st.embedPost.dateFg;
} else if (segment.block
&& (segment.block->kind == PreparedBlockKind::Thinking)) {
return st.supplementaryTextColor;
} else if (segment.kind == SelectableSegmentKind::CodeBlock
|| (segment.block
&& segment.block->kind == PreparedBlockKind::CodeBlock)) {
return st.textPalette.monoFg;
} else if (segment.block && segment.block->supplementary) {
return st.supplementaryTextColor;
}
return st.textColor;
}
bool TableSegmentSelected(
const PaintSelectionState &selectionState,
int tableSegmentIndex) {
if (!selectionState.segments || tableSegmentIndex < 0) {
return false;
}
if (selectionState.hasStructuralSelection()) {
if (const auto segment = FindSegment(
selectionState.segments,
tableSegmentIndex);
segment && WholeSegmentStructurallySelected(
*segment,
selectionState)) {
return true;
}
}
if (SingleTableCellSelection(selectionState, tableSegmentIndex)) {
return false;
}
const auto normalized = NormalizeSelection(selectionState.selection);
if (normalized.empty()) {
return false;
}
auto selectedCells = 0;
auto selectedCellIndex = -1;
for (const auto &segment : *selectionState.segments) {
if (segment.tableSegmentIndex != tableSegmentIndex
|| segment.index == tableSegmentIndex) {
continue;
}
const auto textSelection = BaseTextSelectionForSegment(
segment,
normalized);
if (!textSelection || textSelection->empty()) {
continue;
}
if (++selectedCells == 1) {
selectedCellIndex = segment.index;
} else {
return true;
}
}
const auto table = FindSegment(selectionState.segments, tableSegmentIndex);
if (!table || !RangeSelectsWholeSegment(*table, normalized)) {
return false;
}
if (selectedCells != 1) {
return true;
}
if (normalized.from.segment == tableSegmentIndex
|| normalized.to.segment == tableSegmentIndex) {
return true;
}
const auto lower = std::min(tableSegmentIndex, selectedCellIndex);
const auto upper = std::max(tableSegmentIndex, selectedCellIndex);
return (normalized.from.segment < lower)
&& (normalized.to.segment > upper);
}
bool StructuralBlockSelected(
const PaintSelectionState &selectionState,
const PreparedEditBlockSource &source) {
const auto selection = StructuralSelection(
selectionState,
PreparedEditSelectionKind::Blocks);
if (!selection) {
return false;
}
const auto &range = selection->blocks;
return (source.path.container == range.container)
&& IndexInRange(source.path.index, range.from, range.till);
}
bool StructuralListItemSelected(
const PaintSelectionState &selectionState,
const PreparedEditListItemSource &source) {
const auto selection = StructuralSelection(
selectionState,
PreparedEditSelectionKind::ListItems);
if (!selection) {
return false;
}
const auto &range = selection->listItems;
return (source.block == range.block)
&& IndexInRange(source.listItemIndex, range.from, range.till);
}
bool StructuralTableRowSelected(
const PaintSelectionState &selectionState,
const PreparedEditTableRowSource &source) {
const auto selection = StructuralSelection(
selectionState,
PreparedEditSelectionKind::TableRows);
if (!selection) {
return false;
}
const auto &range = selection->tableRows;
return (source.block == range.block)
&& IndexInRange(source.tableRowIndex, range.from, range.till);
}
bool StructuralTableCellSelected(
const PaintSelectionState &selectionState,
const PreparedEditTableCellSource &source) {
const auto selection = StructuralSelection(
selectionState,
PreparedEditSelectionKind::TableCells);
if (!selection) {
return false;
}
const auto &range = selection->tableCells;
if (range.empty()
|| source.tableRowIndex < 0
|| source.column < 0
|| source.colspan <= 0
|| source.rowspan <= 0) {
return false;
}
return (source.block == range.block)
&& RangesIntersect(
source.tableRowIndex,
source.tableRowIndex + source.rowspan,
range.rowFrom,
range.rowTill)
&& RangesIntersect(
source.column,
source.column + source.colspan,
range.columnFrom,
range.columnTill);
}
std::optional<TextSelection> TextSelectionForSegment(
const SelectableSegment &segment,
const PaintSelectionState &selectionState) {
return ResolveTextSelectionForSegment(
segment,
selectionState,
false);
}
std::optional<TextSelection> TextSelectionForSegmentIndex(
const PaintSelectionState &selectionState,
int index) {
const auto segment = FindSegment(selectionState.segments, index);
return segment
? TextSelectionForSegment(*segment, selectionState)
: std::nullopt;
}
std::optional<TextSelection> PaintTextSelectionForSegment(
const SelectableSegment &segment,
const PaintSelectionState &selectionState) {
return ResolveTextSelectionForSegment(
segment,
selectionState,
true);
}
std::optional<TextSelection> PaintTextSelectionForSegmentIndex(
const PaintSelectionState &selectionState,
int index) {
const auto segment = FindSegment(selectionState.segments, index);
return segment
? PaintTextSelectionForSegment(*segment, selectionState)
: std::nullopt;
}
bool WholeSegmentSelected(
const SelectableSegment &segment,
const PaintSelectionState &selectionState) {
if (!selectionState.segments || segment.isTextLeaf()) {
return false;
}
if (WholeSegmentStructurallySelected(segment, selectionState)) {
return true;
}
if (segment.kind == SelectableSegmentKind::Table) {
return TableSegmentSelected(selectionState, segment.index);
}
return RangeSelectsWholeSegment(segment, selectionState.selection);
}
bool WholeSegmentSelected(
const PaintSelectionState &selectionState,
int index) {
const auto segment = FindSegment(selectionState.segments, index);
return segment
? WholeSegmentSelected(*segment, selectionState)
: false;
}
TextForMimeData TextForSegment(
const SelectableSegment &segment,
TextSelection selection) {
switch (segment.kind) {
case SelectableSegmentKind::TextLeaf:
return segment.leaf
? segment.leaf->toTextForMimeData(selection)
: TextForMimeData();
case SelectableSegmentKind::CodeBlock:
return segment.block
? CopyTextForCodeBlock(*segment.block, selection)
: TextForMimeData();
case SelectableSegmentKind::DisplayMath:
return segment.block
? CopyTextForDisplayMath(*segment.block)
: TextForMimeData();
case SelectableSegmentKind::Table:
return segment.block
? CopyTextForTable(*segment.block)
: TextForMimeData();
case SelectableSegmentKind::Placeholder:
return segment.block
? CopyTextForPlaceholderBlock(*segment.block)
: TextForMimeData();
case SelectableSegmentKind::Photo:
return segment.block
? CopyTextForPhotoBlock(*segment.block)
: TextForMimeData();
case SelectableSegmentKind::Media:
return segment.block
? CopyTextForSingleMediaBlock(*segment.block)
: TextForMimeData();
}
return TextForMimeData();
}
TextForMimeData TextForSelectedSegments(
const std::vector<SelectableSegment> &segments,
MarkdownArticleSelection selection,
const MarkdownArticleSelectionEndpoints *endpoints,
const PreparedEditSelection *structuralSelection) {
if (structuralSelection
&& !structuralSelection->empty()
&& (structuralSelection->kind == PreparedEditSelectionKind::TableRows
|| structuralSelection->kind
== PreparedEditSelectionKind::TableCells)) {
return TextForStructuralTableSelection(segments, *structuralSelection);
}
if (selection.empty()
&& (!structuralSelection || structuralSelection->empty())) {
return TextForMimeData();
}
const auto selectionState = PaintSelectionState{
.segments = &segments,
.selection = selection,
.endpoints = endpoints,
.structuralSelection = structuralSelection,
};
auto pieces = std::vector<TextForMimeData>();
for (const auto &segment : segments) {
if (segment.isTextLeaf()) {
if (segment.parentSegmentIndex >= 0
&& WholeSegmentSelected(
selectionState,
segment.parentSegmentIndex)) {
continue;
}
if (const auto textSelection = TextSelectionForSegment(
segment,
selectionState);
textSelection && !textSelection->empty()) {
if (auto text = TextForSegment(segment, *textSelection);
!text.empty()) {
pieces.push_back(std::move(text));
}
}
continue;
}
if (!WholeSegmentSelected(segment, selectionState)) {
continue;
}
if (auto text = TextForSegment(segment); !text.empty()) {
pieces.push_back(std::move(text));
}
}
if (pieces.empty()) {
return TextForMimeData();
} else if (pieces.size() == 1) {
return std::move(pieces.front());
}
auto result = TextForMimeData();
for (auto i = 0, count = int(pieces.size()); i != count; ++i) {
if (i) {
result.append(u"\n"_q);
}
result.append(std::move(pieces[i]));
}
return result;
}
} // namespace Iv::Markdown