diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp index 31b2338418..517dc199d6 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp @@ -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(); - 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(); + 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 State::removeActiveOwnerAndSelectAdjacent(bool forward) { @@ -2309,59 +2362,106 @@ std::optional State::adjacentEditableOrdinal(bool forward) const { : std::nullopt; } -void State::collectBoundaryElements( +void State::collectBoundarySteps( const std::vector &blocks, const BlockContainerPath &container, - std::vector *elements) const { - for (auto i = 0, count = int(blocks.size()); i != count; ++i) { + bool forward, + std::vector *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 *elements) const { + std::vector *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 *elements) const { + std::vector *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 *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 { diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_state.h b/Telegram/SourceFiles/iv/editor/iv_editor_state.h index 600b6739ca..0b989d3cb7 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_state.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_state.h @@ -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 moveActiveQuoteDown(); [[nodiscard]] std::optional handleActiveHeadingEnter(); [[nodiscard]] std::optional handleActiveListEnter(); @@ -208,17 +215,6 @@ public: void insertPreparedBlocksAfterActive(std::vector 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 adjacentEditableOrdinal( bool forward) const; - void collectBoundaryElements( + void collectBoundarySteps( const std::vector &blocks, const BlockContainerPath &container, - std::vector *elements) const; - void appendBoundaryTextElement( + bool forward, + std::vector *steps) const; + void appendBoundaryTextStep( LeafPath leaf, - std::vector *elements) const; - void appendBoundaryBlockElement( + std::vector *steps) const; + void appendBoundaryBlockStep( const BlockPath &path, - std::vector *elements) const; + std::vector *steps) const; + void appendBoundaryListItemStep( + const BlockPath &path, + int itemIndex, + std::vector *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; diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp index c051f435e8..46a7b50e69 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp @@ -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(parent)) { + scrollIn(scroll); + return; + } + if (const auto scroll = dynamic_cast(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()); } diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_widget.h b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h index 01c2abba9d..71f5a5a1ed 100644 --- a/Telegram/SourceFiles/iv/editor/iv_editor_widget.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h @@ -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; + Ui::VisibleRange _visibleRange; ArticleSelectionDrag _articleSelectionDrag; std::optional _horizontalScrollLock; bool _settingField = false; diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp index b9596b026c..2afd62ce87 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.cpp @@ -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 *prepared, QPoint point, std::vector *preparedPath) const; + [[nodiscard]] std::optional + findHorizontalScrollOwner(int segmentIndex) const; + [[nodiscard]] std::optional + findHorizontalScrollOwner( + const std::vector &blocks, + const std::vector *prepared, + const SelectableSegment &segment, + std::optional owner, + std::vector *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 +MarkdownArticle::Impl::findHorizontalScrollOwner(int segmentIndex) const { + const auto segment = FindSegment(&_segments, segmentIndex); + if (!segment || !IsEditableSegment(*segment)) { + return std::nullopt; + } + auto preparedPath = std::vector(); + return findHorizontalScrollOwner( + _blocks, + &_content.blocks.blocks, + *segment, + std::nullopt, + &preparedPath); +} + +std::optional +MarkdownArticle::Impl::findHorizontalScrollOwner( + const std::vector &blocks, + const std::vector *prepared, + const SelectableSegment &segment, + std::optional owner, + std::vector *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(); @@ -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); diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h index 94b527e6f6..d192855418 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article.h @@ -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(