mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: compute cursor and selection by [Selection] or [Offset]
This commit is contained in:
parent
114ae2b45d
commit
cde2127dec
@ -326,7 +326,7 @@ TextSelection? _globalSelectionToLocal(Node node, Selection? globalSel) {
|
||||
if (!pathEquals(nodePath, globalSel.start.path)) {
|
||||
return null;
|
||||
}
|
||||
if (globalSel.isCollapsed()) {
|
||||
if (globalSel.isCollapsed) {
|
||||
return TextSelection(
|
||||
baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset);
|
||||
} else {
|
||||
|
@ -7,27 +7,3 @@ typedef Path = List<int>;
|
||||
bool pathEquals(Path path1, Path path2) {
|
||||
return listEquals(path1, path2);
|
||||
}
|
||||
|
||||
/// Returns true if path1 >= path2, otherwise returns false.
|
||||
/// TODO: Rename this function.
|
||||
bool pathGreaterOrEquals(Path path1, Path path2) {
|
||||
final length = min(path1.length, path2.length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (path1[i] < path2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Returns true if path1 <= path2, otherwise returns false.
|
||||
/// TODO: Rename this function.
|
||||
bool pathLessOrEquals(Path path1, Path path2) {
|
||||
final length = min(path1.length, path2.length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (path1[i] > path2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -31,4 +31,7 @@ class Position {
|
||||
offset: offset ?? this.offset,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'path = $path, offset = $offset';
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:flowy_editor/document/path.dart';
|
||||
import 'package:flowy_editor/document/position.dart';
|
||||
import 'package:flowy_editor/extensions/path_extensions.dart';
|
||||
|
||||
class Selection {
|
||||
final Position start;
|
||||
@ -29,9 +30,11 @@ class Selection {
|
||||
}
|
||||
}
|
||||
|
||||
bool isCollapsed() {
|
||||
return start == end;
|
||||
}
|
||||
bool get isCollapsed => start == end;
|
||||
bool get isUpward =>
|
||||
start.path >= end.path && !pathEquals(start.path, end.path);
|
||||
bool get isDownward =>
|
||||
start.path <= end.path && !pathEquals(start.path, end.path);
|
||||
|
||||
Selection copyWith({Position? start, Position? end}) {
|
||||
return Selection(
|
||||
@ -39,4 +42,7 @@ class Selection {
|
||||
end: end ?? this.end,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '[Selection] start = $start, end = $end';
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/document/selection.dart';
|
||||
import 'package:flowy_editor/extensions/object_extensions.dart';
|
||||
import 'package:flowy_editor/extensions/path_extensions.dart';
|
||||
import 'package:flowy_editor/render/selection/selectable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@ -8,4 +12,12 @@ extension NodeExtensions on Node {
|
||||
key?.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>();
|
||||
|
||||
Selectable? get selectable => key?.currentState?.unwrapOrNull<Selectable>();
|
||||
|
||||
bool inSelection(Selection selection) {
|
||||
if (selection.start.path <= selection.end.path) {
|
||||
return selection.start.path <= path && path <= selection.end.path;
|
||||
} else {
|
||||
return selection.end.path <= path && path <= selection.start.path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
import 'package:flowy_editor/document/path.dart';
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
extension PathExtensions on Path {
|
||||
bool operator >=(Path other) {
|
||||
final length = min(this.length, other.length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (this[i] < other[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool operator <=(Path other) {
|
||||
final length = min(this.length, other.length);
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (this[i] > other[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import 'package:flowy_editor/document/path.dart';
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/document/position.dart';
|
||||
import 'package:flowy_editor/document/selection.dart';
|
||||
import 'package:flowy_editor/render/selection/cursor_widget.dart';
|
||||
@ -6,18 +7,22 @@ import 'package:flowy_editor/render/selection/flowy_selection_widget.dart';
|
||||
import 'package:flowy_editor/extensions/object_extensions.dart';
|
||||
import 'package:flowy_editor/extensions/node_extensions.dart';
|
||||
import 'package:flowy_editor/service/shortcut_service.dart';
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../editor_state.dart';
|
||||
import '../document/node.dart';
|
||||
import '../render/selection/selectable.dart';
|
||||
|
||||
/// Process selection and cursor
|
||||
mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
||||
///
|
||||
List<Node> get currentSelectedNodes;
|
||||
|
||||
///
|
||||
void updateSelection(Selection selection);
|
||||
|
||||
///
|
||||
void clearSelection();
|
||||
|
||||
/// Returns selected [Node]s. Empty list would be returned
|
||||
/// if no nodes are being selected.
|
||||
///
|
||||
@ -49,7 +54,7 @@ mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
||||
/// Return [bool] to identify the [Node] is in Range or not.
|
||||
///
|
||||
/// [start] and [end] are the offsets under the global coordinate system.
|
||||
bool isNodeInSelection(
|
||||
bool isNodeInRange(
|
||||
Node node,
|
||||
Offset start,
|
||||
Offset end,
|
||||
@ -96,6 +101,12 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
|
||||
EditorState get editorState => widget.editorState;
|
||||
|
||||
Node? _selectedNodeInPostion(Node node, Position position) =>
|
||||
node.childAtPath(position.path);
|
||||
|
||||
@override
|
||||
List<Node> currentSelectedNodes = [];
|
||||
|
||||
@override
|
||||
List<Node> getNodesInSelection(Selection selection) =>
|
||||
_selectedNodesInSelection(editorState.document.root, selection);
|
||||
@ -129,16 +140,21 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
|
||||
@override
|
||||
void updateSelection(Selection selection) {
|
||||
_clearAllOverlayEntries();
|
||||
_clearSelection();
|
||||
|
||||
// cursor
|
||||
if (selection.isCollapsed()) {
|
||||
if (selection.isCollapsed) {
|
||||
_updateCursor(selection.start);
|
||||
} else {
|
||||
_updateSelection(selection);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void clearSelection() {
|
||||
_clearSelection();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Node> getNodesInRange(Offset start, [Offset? end]) {
|
||||
if (end != null) {
|
||||
@ -172,7 +188,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
List<Node> computeNodesInRange(Node node, Offset start, Offset end) {
|
||||
List<Node> result = [];
|
||||
if (node.parent != null && node.key != null) {
|
||||
if (isNodeInSelection(node, start, end)) {
|
||||
if (isNodeInRange(node, start, end)) {
|
||||
result.add(node);
|
||||
}
|
||||
}
|
||||
@ -195,7 +211,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
}
|
||||
|
||||
@override
|
||||
bool isNodeInSelection(Node node, Offset start, Offset end) {
|
||||
bool isNodeInRange(Node node, Offset start, Offset end) {
|
||||
final renderBox = node.renderBox;
|
||||
if (renderBox != null) {
|
||||
final rect = Rect.fromPoints(start, end);
|
||||
@ -244,10 +260,21 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
final first = nodes.first.selectable;
|
||||
final last = nodes.last.selectable;
|
||||
if (first != null && last != null) {
|
||||
final selection = Selection(
|
||||
start: first.getSelectionInRange(panStartOffset!, panEndOffset!).start,
|
||||
end: last.getSelectionInRange(panStartOffset!, panEndOffset!).end,
|
||||
);
|
||||
final Selection selection;
|
||||
if (panStartOffset!.dy <= panEndOffset!.dy) {
|
||||
// down
|
||||
selection = Selection(
|
||||
start:
|
||||
first.getSelectionInRange(panStartOffset!, panEndOffset!).start,
|
||||
end: last.getSelectionInRange(panStartOffset!, panEndOffset!).end,
|
||||
);
|
||||
} else {
|
||||
// up
|
||||
selection = Selection(
|
||||
start: last.getSelectionInRange(panStartOffset!, panEndOffset!).end,
|
||||
end: first.getSelectionInRange(panStartOffset!, panEndOffset!).start,
|
||||
);
|
||||
}
|
||||
updateSelection(selection);
|
||||
}
|
||||
}
|
||||
@ -256,35 +283,29 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
// do nothing
|
||||
}
|
||||
|
||||
void _clearAllOverlayEntries() {
|
||||
_clearSelection();
|
||||
_clearCursor();
|
||||
_clearFloatingShorts();
|
||||
}
|
||||
|
||||
void _clearSelection() {
|
||||
currentSelectedNodes = [];
|
||||
|
||||
// clear selection
|
||||
_selectionOverlays
|
||||
..forEach((overlay) => overlay.remove())
|
||||
..clear();
|
||||
}
|
||||
|
||||
void _clearCursor() {
|
||||
// clear cursors
|
||||
_cursorOverlays
|
||||
..forEach((overlay) => overlay.remove())
|
||||
..clear();
|
||||
}
|
||||
|
||||
void _clearFloatingShorts() {
|
||||
final shortcutService = editorState
|
||||
.service.floatingShortcutServiceKey.currentState
|
||||
?.unwrapOrNull<FlowyFloatingShortcutService>();
|
||||
shortcutService?.hide();
|
||||
// clear floating shortcusts
|
||||
editorState.service.floatingShortcutServiceKey.currentState
|
||||
?.unwrapOrNull<FlowyFloatingShortcutService>()
|
||||
?.hide();
|
||||
}
|
||||
|
||||
void _updateSelection(Selection selection) {
|
||||
final nodes =
|
||||
_selectedNodesInSelection(editorState.document.root, selection);
|
||||
|
||||
currentSelectedNodes = nodes;
|
||||
|
||||
var index = 0;
|
||||
for (final node in nodes) {
|
||||
final selectable = node.selectable;
|
||||
@ -293,20 +314,38 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
}
|
||||
|
||||
Selection newSelection;
|
||||
// TODO: too complicate, need to refactor.
|
||||
if (node is TextNode) {
|
||||
if (pathEquals(selection.start.path, selection.end.path)) {
|
||||
newSelection = selection.copyWith();
|
||||
} else {
|
||||
if (index == 0) {
|
||||
newSelection = selection.copyWith(
|
||||
/// FIXME: make it better.
|
||||
end: selection.start.copyWith(offset: node.toRawString().length),
|
||||
);
|
||||
if (selection.isUpward) {
|
||||
newSelection = selection.copyWith(
|
||||
/// FIXME: make it better.
|
||||
start: selection.end.copyWith(),
|
||||
end: selection.end.copyWith(offset: node.toRawString().length),
|
||||
);
|
||||
} else {
|
||||
newSelection = selection.copyWith(
|
||||
/// FIXME: make it better.
|
||||
end:
|
||||
selection.start.copyWith(offset: node.toRawString().length),
|
||||
);
|
||||
}
|
||||
} else if (index == nodes.length - 1) {
|
||||
newSelection = selection.copyWith(
|
||||
/// FIXME: make it better.
|
||||
start: selection.end.copyWith(offset: 0),
|
||||
);
|
||||
if (selection.isUpward) {
|
||||
newSelection = selection.copyWith(
|
||||
/// FIXME: make it better.
|
||||
start: selection.start.copyWith(offset: 0),
|
||||
end: selection.start.copyWith(),
|
||||
);
|
||||
} else {
|
||||
newSelection = selection.copyWith(
|
||||
/// FIXME: make it better.
|
||||
start: selection.end.copyWith(offset: 0),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final position = Position(path: node.path);
|
||||
newSelection = Selection(
|
||||
@ -339,13 +378,15 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
}
|
||||
|
||||
void _updateCursor(Position position) {
|
||||
final node = _selectedNodeInPostion(editorState.document.root, position);
|
||||
final node = editorState.document.root.childAtPath(position.path);
|
||||
|
||||
assert(node != null);
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentSelectedNodes = [node];
|
||||
|
||||
final selectable = node.selectable;
|
||||
final rect = selectable?.getCursorRectInPosition(position);
|
||||
if (rect != null) {
|
||||
@ -365,7 +406,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
List<Node> _selectedNodesInSelection(Node node, Selection selection) {
|
||||
List<Node> result = [];
|
||||
if (node.parent != null) {
|
||||
if (_isNodeInSelection(node, selection)) {
|
||||
if (node.inSelection(selection)) {
|
||||
result.add(node);
|
||||
}
|
||||
}
|
||||
@ -374,12 +415,4 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Node? _selectedNodeInPostion(Node node, Position position) =>
|
||||
node.childAtPath(position.path);
|
||||
|
||||
bool _isNodeInSelection(Node node, Selection selection) {
|
||||
return pathGreaterOrEquals(node.path, selection.start.path) &&
|
||||
pathLessOrEquals(node.path, selection.end.path);
|
||||
}
|
||||
}
|
||||
|
@ -127,7 +127,7 @@ void main() {
|
||||
final pos = Position(path: [0], offset: 0);
|
||||
final sel = Selection.collapsed(pos);
|
||||
expect(sel.start, sel.end);
|
||||
expect(sel.isCollapsed(), true);
|
||||
expect(sel.isCollapsed, true);
|
||||
});
|
||||
|
||||
test('test selection collapse', () {
|
||||
|
Loading…
Reference in New Issue
Block a user