Support better navigation, show edited leaf.

This commit is contained in:
John Preston
2026-06-02 13:44:34 +04:00
parent 2d372c29cc
commit 2cbb2425d4
6 changed files with 556 additions and 137 deletions
+291 -110
View File
@@ -17,6 +17,7 @@ using Block = RichPage::Block;
using BlockContainerKind = State::BlockContainerKind;
using BlockContainerPath = State::BlockContainerPath;
using BlockKind = RichPage::BlockKind;
using BoundaryAction = State::BoundaryTarget::Action;
using BlockPath = State::BlockPath;
using FieldMode = State::FieldMode;
using InsertBlockType = State::InsertBlockType;
@@ -29,6 +30,7 @@ using PreparedBlockContainerKind = Markdown::PreparedEditBlockContainerKind;
using PreparedEditLeafKind = Markdown::PreparedEditLeafKind;
using PreparedEditSelectionKind = Markdown::PreparedEditSelectionKind;
using PreparedBlockContainerPath = Markdown::PreparedEditBlockContainerPath;
using PreparedBlockPath = Markdown::PreparedEditBlockPath;
using PreparedBlockContainerStep = Markdown::PreparedEditBlockContainerStep;
using PreparedEditSelection = Markdown::PreparedEditSelection;
using RemovalKind = State::RemovalKind;
@@ -469,35 +471,59 @@ State::BoundaryTarget State::activeBoundaryTarget(bool forward) const {
if (!descriptor) {
return {};
}
auto elements = std::vector<BoundaryElement>();
collectBoundaryElements(
_richPage->blocks,
BlockContainerPath(),
&elements);
auto current = -1;
for (auto i = 0, count = int(elements.size()); i != count; ++i) {
const auto &element = elements[i];
if (element.kind == BoundaryElement::Kind::Text
&& element.textOrdinal == _activeTextOrdinal) {
current = i;
const auto prioritizeStructuralStep = [&](const BoundaryTarget &target) {
if (target.action != BoundaryAction::StructuralSelection) {
return false;
}
switch (target.structuralSelection.kind) {
case PreparedEditSelectionKind::Blocks:
if (const auto range = validateBlockRange(
target.structuralSelection.blocks)) {
return leafWillBeRemoved(descriptor->leaf, *range);
}
break;
case PreparedEditSelectionKind::ListItems:
if (const auto range = validateListItemRange(
target.structuralSelection.listItems)) {
return leafWillBeRemoved(descriptor->leaf, *range);
}
break;
default:
break;
}
}
if (current < 0) {
return {};
}
const auto adjacentIndex = current + (forward ? 1 : -1);
if (adjacentIndex < 0 || adjacentIndex >= int(elements.size())) {
return {};
}
const auto &adjacent = elements[adjacentIndex];
if (adjacent.kind == BoundaryElement::Kind::Text) {
return { .textOrdinal = adjacent.textOrdinal };
}
return {
.textOrdinal = -1,
.structuralSelection = preparedSelectionForBlock(adjacent.block),
return false;
};
const auto removeDirectly = removalTargetIsEmpty(descriptor->removalTarget)
&& shouldRemoveActiveOwnerDirectly(*descriptor);
auto steps = std::vector<BoundaryTarget>();
collectBoundarySteps(
_richPage->blocks,
BlockContainerPath(),
forward,
&steps);
for (auto i = 0, count = int(steps.size()); i != count; ++i) {
const auto &step = steps[i];
if (step.action == BoundaryAction::Text
&& step.textOrdinal == _activeTextOrdinal) {
const auto next = (i + 1 < count)
? steps[i + 1]
: BoundaryTarget();
if (prioritizeStructuralStep(next)) {
return next;
}
if (removeDirectly) {
return {
.action = BoundaryAction::RemoveActiveOwner,
};
}
return next;
}
}
return removeDirectly
? BoundaryTarget{
.action = BoundaryAction::RemoveActiveOwner,
}
: BoundaryTarget();
}
bool State::isActiveTopLevelParagraph() const {
@@ -552,9 +578,36 @@ bool State::activeLeafUsesQuotePlaceholderColor() const {
return false;
}
bool State::activeOwnerIsEmpty() const {
const auto descriptor = textNode(_activeTextOrdinal);
return descriptor && removalTargetIsEmpty(descriptor->removalTarget);
bool State::shouldRemoveActiveOwnerDirectly(
const TextNodeDescriptor &descriptor) const {
switch (descriptor.removalTarget.kind) {
case RemovalKind::Block: {
const auto owner = block(descriptor.removalTarget.block);
if (!owner) {
return false;
}
switch (owner->kind) {
case BlockKind::Heading:
case BlockKind::Paragraph:
case BlockKind::Footer:
case BlockKind::Code:
case BlockKind::Math:
return true;
default:
return false;
}
}
case RemovalKind::ListItem:
if (const auto owner = listItem(
descriptor.removalTarget.block,
descriptor.removalTarget.listItemIndex)) {
return owner->blocks.empty();
}
return false;
case RemovalKind::TableCell:
return false;
}
return false;
}
std::optional<int> State::removeActiveOwnerAndSelectAdjacent(bool forward) {
@@ -2309,59 +2362,106 @@ std::optional<int> State::adjacentEditableOrdinal(bool forward) const {
: std::nullopt;
}
void State::collectBoundaryElements(
void State::collectBoundarySteps(
const std::vector<Block> &blocks,
const BlockContainerPath &container,
std::vector<BoundaryElement> *elements) const {
for (auto i = 0, count = int(blocks.size()); i != count; ++i) {
bool forward,
std::vector<BoundaryTarget> *steps) const {
const auto collectBlock = [&](int index) {
const auto path = BlockPath{
.container = container,
.index = i,
.index = index,
};
const auto &block = blocks[i];
const auto &block = blocks[index];
switch (block.kind) {
case BlockKind::Heading:
case BlockKind::Paragraph:
case BlockKind::Footer:
case BlockKind::Code:
appendBoundaryTextElement({
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
}, elements);
}, steps);
break;
case BlockKind::Quote:
if (block.blocks.empty()) {
appendBoundaryTextElement({
.kind = LeafKind::BlockText,
if (forward) {
if (block.blocks.empty()) {
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
}, steps);
}
appendBoundaryTextStep({
.kind = LeafKind::BlockCaption,
.block = path,
}, elements);
}, steps);
collectBoundarySteps(
block.blocks,
BlockChildrenContainer(path),
forward,
steps);
} else {
collectBoundarySteps(
block.blocks,
BlockChildrenContainer(path),
forward,
steps);
appendBoundaryTextStep({
.kind = LeafKind::BlockCaption,
.block = path,
}, steps);
if (block.blocks.empty()) {
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
}, steps);
}
}
appendBoundaryTextElement({
.kind = LeafKind::BlockCaption,
.block = path,
}, elements);
collectBoundaryElements(
block.blocks,
BlockChildrenContainer(path),
elements);
appendBoundaryBlockStep(path, steps);
break;
case BlockKind::List:
for (auto j = 0, itemCount = int(block.listItems.size());
j != itemCount;
++j) {
const auto &item = block.listItems[j];
if (!RichTextIsEmpty(item.text) || item.blocks.empty()) {
appendBoundaryTextElement({
.kind = LeafKind::ListItemText,
.block = path,
.listItemIndex = j,
}, elements);
if (forward) {
for (auto j = 0, itemCount = int(block.listItems.size());
j != itemCount;
++j) {
const auto &item = block.listItems[j];
if (!RichTextIsEmpty(item.text) || item.blocks.empty()) {
appendBoundaryTextStep({
.kind = LeafKind::ListItemText,
.block = path,
.listItemIndex = j,
}, steps);
}
if (!item.blocks.empty()) {
collectBoundarySteps(
item.blocks,
ListItemChildrenContainer(path, j),
forward,
steps);
appendBoundaryListItemStep(path, j, steps);
}
}
if (!item.blocks.empty()) {
collectBoundaryElements(
item.blocks,
ListItemChildrenContainer(path, j),
elements);
} else {
for (auto j = int(block.listItems.size()); j != 0; --j) {
const auto itemIndex = j - 1;
const auto &item = block.listItems[itemIndex];
if (!item.blocks.empty()) {
collectBoundarySteps(
item.blocks,
ListItemChildrenContainer(path, itemIndex),
forward,
steps);
}
if (!RichTextIsEmpty(item.text) || item.blocks.empty()) {
appendBoundaryTextStep({
.kind = LeafKind::ListItemText,
.block = path,
.listItemIndex = itemIndex,
}, steps);
}
if (!item.blocks.empty()) {
appendBoundaryListItemStep(path, itemIndex, steps);
}
}
}
break;
@@ -2369,90 +2469,155 @@ void State::collectBoundaryElements(
case BlockKind::Video:
case BlockKind::Audio:
case BlockKind::Map:
appendBoundaryTextElement({
appendBoundaryTextStep({
.kind = LeafKind::BlockCaption,
.block = path,
}, elements);
}, steps);
appendBoundaryBlockStep(path, steps);
break;
case BlockKind::Math:
appendBoundaryTextElement({
appendBoundaryTextStep({
.kind = LeafKind::MathFormula,
.block = path,
}, elements);
}, steps);
break;
case BlockKind::Table:
if (!block.text.text.text.isEmpty()) {
appendBoundaryTextElement({
.kind = LeafKind::BlockText,
.block = path,
}, elements);
}
for (auto j = 0, rowCount = int(block.tableRows.size());
j != rowCount;
++j) {
const auto &row = block.tableRows[j];
for (auto k = 0, cellCount = int(row.cells.size());
k != cellCount;
++k) {
appendBoundaryTextElement({
.kind = LeafKind::TableCellText,
if (forward) {
if (!block.text.text.text.isEmpty()) {
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
.tableRowIndex = j,
.tableCellIndex = k,
}, elements);
}, steps);
}
for (auto j = 0, rowCount = int(block.tableRows.size());
j != rowCount;
++j) {
const auto &row = block.tableRows[j];
for (auto k = 0, cellCount = int(row.cells.size());
k != cellCount;
++k) {
appendBoundaryTextStep({
.kind = LeafKind::TableCellText,
.block = path,
.tableRowIndex = j,
.tableCellIndex = k,
}, steps);
}
}
} else {
for (auto j = int(block.tableRows.size()); j != 0; --j) {
const auto rowIndex = j - 1;
const auto &row = block.tableRows[rowIndex];
for (auto k = int(row.cells.size()); k != 0; --k) {
appendBoundaryTextStep({
.kind = LeafKind::TableCellText,
.block = path,
.tableRowIndex = rowIndex,
.tableCellIndex = k - 1,
}, steps);
}
}
if (!block.text.text.text.isEmpty()) {
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
}, steps);
}
}
appendBoundaryBlockStep(path, steps);
break;
case BlockKind::Details:
appendBoundaryTextElement({
.kind = LeafKind::BlockText,
.block = path,
}, elements);
collectBoundaryElements(
block.blocks,
BlockChildrenContainer(path),
elements);
break;
default: {
const auto before = elements->size();
if (!block.blocks.empty()) {
collectBoundaryElements(
if (forward) {
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
}, steps);
collectBoundarySteps(
block.blocks,
BlockChildrenContainer(path),
elements);
forward,
steps);
} else {
collectBoundarySteps(
block.blocks,
BlockChildrenContainer(path),
forward,
steps);
appendBoundaryTextStep({
.kind = LeafKind::BlockText,
.block = path,
}, steps);
}
if (elements->size() == before) {
appendBoundaryBlockElement(path, elements);
appendBoundaryBlockStep(path, steps);
break;
default: {
const auto before = steps->size();
if (!block.blocks.empty()) {
collectBoundarySteps(
block.blocks,
BlockChildrenContainer(path),
forward,
steps);
}
if (steps->size() == before) {
appendBoundaryBlockStep(path, steps);
}
} break;
}
};
if (forward) {
for (auto i = 0, count = int(blocks.size()); i != count; ++i) {
collectBlock(i);
}
} else {
for (auto i = int(blocks.size()); i != 0; --i) {
collectBlock(i - 1);
}
}
}
void State::appendBoundaryTextElement(
void State::appendBoundaryTextStep(
LeafPath leaf,
std::vector<BoundaryElement> *elements) const {
std::vector<BoundaryTarget> *steps) const {
const auto ordinal = textNodeOrdinal(leaf);
if (ordinal >= 0) {
elements->push_back({
.kind = BoundaryElement::Kind::Text,
steps->push_back({
.action = BoundaryAction::Text,
.textOrdinal = ordinal,
});
}
}
void State::appendBoundaryBlockElement(
void State::appendBoundaryBlockStep(
const BlockPath &path,
std::vector<BoundaryElement> *elements) const {
std::vector<BoundaryTarget> *steps) const {
const auto owner = block(path);
if (owner && CanEditBlock(*owner)) {
elements->push_back({
.kind = BoundaryElement::Kind::Block,
.block = path,
steps->push_back({
.action = BoundaryAction::StructuralSelection,
.structuralSelection = preparedSelectionForBlock(path),
});
}
}
void State::appendBoundaryListItemStep(
const BlockPath &path,
int itemIndex,
std::vector<BoundaryTarget> *steps) const {
const auto owner = block(path);
if (!owner
|| owner->kind != BlockKind::List
|| itemIndex < 0
|| itemIndex >= int(owner->listItems.size())
|| !CanEditBlocks(owner->listItems[itemIndex].blocks)) {
return;
}
steps->push_back({
.action = BoundaryAction::StructuralSelection,
.structuralSelection = preparedSelectionForListItem(path, itemIndex),
});
}
PreparedEditSelection State::preparedSelectionForBlock(
const BlockPath &path) const {
return {
@@ -2465,6 +2630,22 @@ PreparedEditSelection State::preparedSelectionForBlock(
};
}
PreparedEditSelection State::preparedSelectionForListItem(
const BlockPath &path,
int itemIndex) const {
return {
.kind = PreparedEditSelectionKind::ListItems,
.listItems = {
.block = PreparedBlockPath{
.container = ToPreparedBlockContainerPath(path.container),
.index = path.index,
},
.from = itemIndex,
.till = itemIndex + 1,
},
};
}
bool State::descriptorBelongsToBlock(
const TextNodeDescriptor &descriptor,
const BlockPath &path) const {
@@ -155,6 +155,14 @@ public:
};
struct BoundaryTarget {
enum class Action : uchar {
None,
Text,
StructuralSelection,
RemoveActiveOwner,
};
Action action = Action::None;
int textOrdinal = -1;
Markdown::PreparedEditSelection structuralSelection;
};
@@ -187,7 +195,6 @@ public:
[[nodiscard]] bool isActiveTopLevelParagraph() const;
[[nodiscard]] bool activeLeafUsesQuoteCaptionColor() const;
[[nodiscard]] bool activeLeafUsesQuotePlaceholderColor() const;
[[nodiscard]] bool activeOwnerIsEmpty() const;
[[nodiscard]] std::optional<int> moveActiveQuoteDown();
[[nodiscard]] std::optional<int> handleActiveHeadingEnter();
[[nodiscard]] std::optional<int> handleActiveListEnter();
@@ -208,17 +215,6 @@ public:
void insertPreparedBlocksAfterActive(std::vector<RichPage::Block> blocks);
private:
struct BoundaryElement {
enum class Kind : uchar {
Text,
Block,
};
Kind kind = Kind::Text;
int textOrdinal = -1;
BlockPath block;
};
struct StructuralBlockRange {
BlockContainerPath container;
int from = -1;
@@ -384,18 +380,28 @@ private:
int count);
[[nodiscard]] std::optional<int> adjacentEditableOrdinal(
bool forward) const;
void collectBoundaryElements(
void collectBoundarySteps(
const std::vector<RichPage::Block> &blocks,
const BlockContainerPath &container,
std::vector<BoundaryElement> *elements) const;
void appendBoundaryTextElement(
bool forward,
std::vector<BoundaryTarget> *steps) const;
void appendBoundaryTextStep(
LeafPath leaf,
std::vector<BoundaryElement> *elements) const;
void appendBoundaryBlockElement(
std::vector<BoundaryTarget> *steps) const;
void appendBoundaryBlockStep(
const BlockPath &path,
std::vector<BoundaryElement> *elements) const;
std::vector<BoundaryTarget> *steps) const;
void appendBoundaryListItemStep(
const BlockPath &path,
int itemIndex,
std::vector<BoundaryTarget> *steps) const;
[[nodiscard]] Markdown::PreparedEditSelection preparedSelectionForBlock(
const BlockPath &path) const;
[[nodiscard]] Markdown::PreparedEditSelection preparedSelectionForListItem(
const BlockPath &path,
int itemIndex) const;
[[nodiscard]] bool shouldRemoveActiveOwnerDirectly(
const TextNodeDescriptor &descriptor) const;
[[nodiscard]] bool descriptorBelongsToBlock(
const TextNodeDescriptor &descriptor,
const BlockPath &path) const;
@@ -24,7 +24,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/painter.h"
#include "ui/text/text_entity.h"
#include "ui/ui_utility.h"
#include "ui/widgets/elastic_scroll.h"
#include "ui/widgets/fields/input_field.h"
#include "ui/widgets/scroll_area.h"
#include "window/window_session_controller.h"
#include "styles/palette.h"
@@ -864,6 +866,7 @@ int Widget::resizeGetHeight(int newWidth) {
const auto width = std::max(newWidth, 1);
const auto padding = EditorBodyPadding();
_articleHeight = _article->resizeGetHeight(articleWidth(width));
syncArticleVisibleTopBottom();
ensurePendingActivation();
syncInlineFieldGeometry(width);
const auto fieldBottom = (!_field->isHidden())
@@ -876,6 +879,14 @@ int Widget::resizeGetHeight(int newWidth) {
st::ivEditorMinHeight);
}
void Widget::visibleTopBottomUpdated(int visibleTop, int visibleBottom) {
_visibleRange = Ui::VisibleRange{
.top = visibleTop,
.bottom = visibleBottom,
};
syncArticleVisibleTopBottom();
}
bool Widget::eventFilter(QObject *object, QEvent *event) {
if (_field) {
const auto raw = _field->rawTextEdit();
@@ -1804,10 +1815,45 @@ void Widget::activateTextOrdinal(
_field->show();
syncInlineFieldGeometry();
updateInlineFieldHeightOverride();
syncArticleVisibleTopBottom();
revealActiveInlineField();
_field->raise();
_field->setFocusFast();
}
void Widget::revealActiveInlineField() {
if (_field->isHidden() || _activeSegmentIndex < 0) {
return;
}
if (_article->revealSegment(_activeSegmentIndex)) {
syncInlineFieldGeometry();
if (_field->isHidden()) {
return;
}
}
const auto scrollIn = [&](auto &&scroll) {
if (const auto inner = scroll->widget()) {
const auto fieldRect = _field->geometry();
const auto localRect = QRect(
inner->mapFromGlobal(mapToGlobal(fieldRect.topLeft())),
fieldRect.size());
scroll->scrollToY(
localRect.y(),
localRect.y() + localRect.height());
}
};
for (auto parent = parentWidget(); parent; parent = parent->parentWidget()) {
if (const auto scroll = dynamic_cast<Ui::ScrollArea*>(parent)) {
scrollIn(scroll);
return;
}
if (const auto scroll = dynamic_cast<Ui::ElasticScroll*>(parent)) {
scrollIn(scroll);
return;
}
}
}
void Widget::activateTrailingParagraph() {
commitInlineField();
const auto ordinal = _state->ensureTrailingParagraphActive();
@@ -2000,7 +2046,10 @@ bool Widget::moveTabBoundary(bool forward) {
bool Widget::removeBoundaryOwner(bool forward) {
commitInlineField();
if (_state->activeOwnerIsEmpty()) {
const auto target = _state->activeBoundaryTarget(forward);
using BoundaryAction = State::BoundaryTarget::Action;
switch (target.action) {
case BoundaryAction::RemoveActiveOwner: {
_boundarySelectionOrigin = std::nullopt;
const auto target = _state->removeActiveOwnerAndSelectAdjacent(
forward);
@@ -2018,8 +2067,7 @@ bool Widget::removeBoundaryOwner(bool forward) {
}
return true;
}
const auto target = _state->activeBoundaryTarget(forward);
if (target.textOrdinal >= 0) {
case BoundaryAction::Text:
_boundarySelectionOrigin = std::nullopt;
refreshPreparedContent();
if (forward) {
@@ -2028,8 +2076,7 @@ bool Widget::removeBoundaryOwner(bool forward) {
activateTextOrdinalAtEnd(target.textOrdinal);
}
return true;
}
if (!target.structuralSelection.empty()) {
case BoundaryAction::StructuralSelection:
setStructuralSelection(
target.structuralSelection,
BoundarySelectionOrigin{
@@ -2046,6 +2093,8 @@ bool Widget::removeBoundaryOwner(bool forward) {
refreshPreparedContent();
update();
return true;
case BoundaryAction::None:
break;
}
return moveBoundaryAfterCommit(forward, forward);
}
@@ -2096,6 +2145,16 @@ void Widget::ensureArticleLayoutForInlineField(int width) {
_articleHeight = _article->resizeGetHeight(articleWidth(width));
}
void Widget::syncArticleVisibleTopBottom() {
if (!_article) {
return;
}
const auto articleTop = articleTopLeft().y();
_article->setVisibleTopBottom(
_visibleRange.top - articleTop,
_visibleRange.bottom - articleTop);
}
void Widget::syncInlineFieldGeometry(int width) {
if (_field->isHidden() || width <= 0) {
return;
@@ -2116,9 +2175,7 @@ void Widget::syncInlineFieldGeometry(int width) {
const auto left = segmentRect.x() - margins.left();
const auto top = segmentRect.y() - margins.top();
const auto fieldWidth = std::max(
std::min(
segmentRect.width() + margins.left() + margins.right(),
width - left),
segmentRect.width() + margins.left() + margins.right(),
1);
_syncingInlineFieldGeometry = true;
_field->resizeToWidth(fieldWidth);
@@ -2731,7 +2788,7 @@ int Widget::articleWidth(int outerWidth) const {
}
QRect Widget::outerEditableSegmentRect(int segmentIndex) const {
const auto rect = _article->segmentRect(segmentIndex);
const auto rect = _article->logicalSegmentRect(segmentIndex);
return rect.isEmpty() ? rect : rect.translated(articleTopLeft());
}
@@ -78,6 +78,7 @@ protected:
void mouseReleaseEvent(QMouseEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void visibleTopBottomUpdated(int visibleTop, int visibleBottom) override;
void wheelEvent(QWheelEvent *e) override;
private:
@@ -195,7 +196,9 @@ private:
void ensurePendingActivation();
void updateInlineFieldHeightOverride();
void ensureArticleLayoutForInlineField(int width);
void syncArticleVisibleTopBottom();
void syncInlineFieldGeometry(int width);
void revealActiveInlineField();
void clearSelection();
void clearTextSelection();
void clearStructuralSelection();
@@ -255,6 +258,7 @@ private:
Markdown::MarkdownArticleSelectionEndpoints _selectionEndpoints;
Markdown::PreparedEditSelection _structuralSelection;
std::optional<BoundarySelectionOrigin> _boundarySelectionOrigin;
Ui::VisibleRange _visibleRange;
ArticleSelectionDrag _articleSelectionDrag;
std::optional<Qt::Orientation> _horizontalScrollLock;
bool _settingField = false;
@@ -726,6 +726,43 @@ void RebuildVisibleSegmentLookup(
};
}
[[nodiscard]] int ComputeScrollTo(
int toFrom,
int toTill,
int toMin,
int toMax,
int current,
int size) {
if (toFrom < toMin) {
toFrom = toMin;
} else if (toFrom > toMax) {
toFrom = toMax;
}
const auto exact = (toTill < 0);
const auto curBottom = current + size;
auto scToFrom = toFrom;
if (!exact && toFrom >= current) {
if (toTill < toFrom) {
toTill = toFrom;
}
if (toTill <= curBottom) {
return current;
}
scToFrom = toTill - size;
if (scToFrom > toFrom) {
scToFrom = toFrom;
}
if (scToFrom == current) {
return current;
}
} else {
scToFrom = toFrom;
}
return scToFrom;
}
[[nodiscard]] QRect LogicalTextRectForSegment(const SelectableSegment &segment) {
if (segment.cell) {
return segment.cell->textRect;
@@ -1757,8 +1794,10 @@ public:
[[nodiscard]] int segmentIndexForEditableIndex(int editableIndex) const;
[[nodiscard]] QRect textSegmentRect(int segmentIndex) const;
[[nodiscard]] QRect logicalSegmentRect(int segmentIndex) const;
[[nodiscard]] QRect segmentRect(int segmentIndex) const;
[[nodiscard]] bool revealSegment(int segmentIndex);
[[nodiscard]] MarkdownArticleTextLeafStyle textLeafStyleForSegment(
int segmentIndex) const;
@@ -1893,6 +1932,15 @@ private:
const std::vector<PreparedBlock> *prepared,
QPoint point,
std::vector<int> *preparedPath) const;
[[nodiscard]] std::optional<MarkdownArticleHorizontalScrollLookup>
findHorizontalScrollOwner(int segmentIndex) const;
[[nodiscard]] std::optional<MarkdownArticleHorizontalScrollLookup>
findHorizontalScrollOwner(
const std::vector<LaidOutBlock> &blocks,
const std::vector<PreparedBlock> *prepared,
const SelectableSegment &segment,
std::optional<MarkdownArticleHorizontalScrollLookup> owner,
std::vector<int> *preparedPath) const;
[[nodiscard]] LaidOutBlock *findScrollOwnerByIdentity(
const MarkdownArticleScrollOwnerIdentity &identity);
[[nodiscard]] LaidOutBlock *findScrollOwnerByIdentity(
@@ -2284,6 +2332,18 @@ QRect MarkdownArticle::Impl::textSegmentRect(int segmentIndex) const {
return (segment && segment->isTextLeaf()) ? segment->textRect : QRect();
}
QRect MarkdownArticle::Impl::logicalSegmentRect(int segmentIndex) const {
const auto segment = FindSegment(&_segments, segmentIndex);
if (!segment) {
return QRect();
} else if (segment->isTextLeaf()) {
return LogicalTextRectForSegment(*segment);
} else if (IsDisplayMathSegment(*segment)) {
return segment->block ? segment->block->formulaRect : QRect();
}
return QRect();
}
QRect MarkdownArticle::Impl::segmentRect(int segmentIndex) const {
const auto segment = FindSegment(&_segments, segmentIndex);
if (!segment) {
@@ -3004,6 +3064,70 @@ MarkdownArticle::Impl::findHorizontalScrollOwner(
return {};
}
std::optional<MarkdownArticleHorizontalScrollLookup>
MarkdownArticle::Impl::findHorizontalScrollOwner(int segmentIndex) const {
const auto segment = FindSegment(&_segments, segmentIndex);
if (!segment || !IsEditableSegment(*segment)) {
return std::nullopt;
}
auto preparedPath = std::vector<int>();
return findHorizontalScrollOwner(
_blocks,
&_content.blocks.blocks,
*segment,
std::nullopt,
&preparedPath);
}
std::optional<MarkdownArticleHorizontalScrollLookup>
MarkdownArticle::Impl::findHorizontalScrollOwner(
const std::vector<LaidOutBlock> &blocks,
const std::vector<PreparedBlock> *prepared,
const SelectableSegment &segment,
std::optional<MarkdownArticleHorizontalScrollLookup> owner,
std::vector<int> *preparedPath) const {
for (auto i = 0, count = int(blocks.size()); i != count; ++i) {
const auto &block = blocks[i];
preparedPath->push_back(i);
const auto preparedBlock = (prepared && (i < prepared->size()))
? &(*prepared)[i]
: nullptr;
auto nextOwner = owner;
if (!block.scrollViewportRect.isEmpty()) {
nextOwner = MarkdownArticleHorizontalScrollLookup{
.identity = scrollOwnerIdentity(block, *preparedPath),
.block = &block,
};
}
const auto matchesCell = [&] {
if (!segment.cell) {
return false;
}
for (const auto &row : block.tableRows) {
for (const auto &cell : row.cells) {
if (segment.cell == &cell) {
return true;
}
}
}
return false;
}();
if (segment.block == &block || matchesCell) {
return nextOwner;
}
if (const auto result = findHorizontalScrollOwner(
block.children,
preparedBlock ? &preparedBlock->children : nullptr,
segment,
nextOwner,
preparedPath)) {
return result;
}
preparedPath->pop_back();
}
return std::nullopt;
}
LaidOutBlock *MarkdownArticle::Impl::findScrollOwnerByIdentity(
const MarkdownArticleScrollOwnerIdentity &identity) {
auto preparedPath = std::vector<int>();
@@ -3221,6 +3345,43 @@ bool MarkdownArticle::Impl::setScrollLeft(
return true;
}
bool MarkdownArticle::Impl::revealSegment(int segmentIndex) {
const auto logicalRect = logicalSegmentRect(segmentIndex);
if (logicalRect.isEmpty()) {
return false;
}
const auto lookup = findHorizontalScrollOwner(segmentIndex);
if (!lookup || !lookup->block) {
return false;
}
const auto &owner = *lookup->block;
if (owner.scrollViewportRect.isEmpty()
|| owner.scrollLogicalContentRect.isEmpty()) {
return false;
}
const auto viewportWidth = owner.scrollViewportRect.width();
if (viewportWidth <= 0) {
return false;
}
const auto toLeft = logicalRect.x()
+ owner.horizontalScrollLeft
- owner.scrollLogicalContentRect.x();
const auto left = ComputeScrollTo(
toLeft,
toLeft + logicalRect.width(),
0,
owner.horizontalScrollMax,
owner.horizontalScrollLeft,
viewportWidth);
if (left == owner.horizontalScrollLeft) {
return false;
}
if (const auto block = findScrollOwnerByIdentity(lookup->identity)) {
return setScrollLeft(*block, lookup->identity, left);
}
return false;
}
MarkdownArticleHorizontalScrollHit MarkdownArticle::Impl::horizontalScrollHit(
QPoint point) const {
return findHorizontalScrollOwner(point).hit;
@@ -3587,10 +3748,18 @@ QRect MarkdownArticle::textSegmentRect(int segmentIndex) const {
return _impl->textSegmentRect(segmentIndex);
}
QRect MarkdownArticle::logicalSegmentRect(int segmentIndex) const {
return _impl->logicalSegmentRect(segmentIndex);
}
QRect MarkdownArticle::segmentRect(int segmentIndex) const {
return _impl->segmentRect(segmentIndex);
}
bool MarkdownArticle::revealSegment(int segmentIndex) {
return _impl->revealSegment(segmentIndex);
}
MarkdownArticleTextLeafStyle MarkdownArticle::textLeafStyleForSegment(
int segmentIndex) const {
return _impl->textLeafStyleForSegment(segmentIndex);
@@ -326,7 +326,9 @@ public:
[[nodiscard]] int editableIndexForSegment(int segmentIndex) const;
[[nodiscard]] int segmentIndexForEditableIndex(int editableIndex) const;
[[nodiscard]] QRect textSegmentRect(int segmentIndex) const;
[[nodiscard]] QRect logicalSegmentRect(int segmentIndex) const;
[[nodiscard]] QRect segmentRect(int segmentIndex) const;
[[nodiscard]] bool revealSegment(int segmentIndex);
[[nodiscard]] MarkdownArticleTextLeafStyle textLeafStyleForSegment(
int segmentIndex) const;
[[nodiscard]] MarkdownArticleTextLeafStyle editableStyleForSegment(