mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-08-11 22:23:49 +00:00
Support better navigation, show edited leaf.
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user