diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index 42bfd1e9f1..a67ebcd2ad 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart @@ -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 { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart index bef96a7bd2..8f24947649 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart @@ -7,27 +7,3 @@ typedef Path = List; 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; -} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart index e213c1eb33..a60f04e89b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart @@ -31,4 +31,7 @@ class Position { offset: offset ?? this.offset, ); } + + @override + String toString() => 'path = $path, offset = $offset'; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart index fe60e1abec..1734fabf24 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart @@ -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'; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart index 35cc18cdd2..49cc38f749 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart @@ -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(); Selectable? get selectable => key?.currentState?.unwrapOrNull(); + + 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; + } + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart new file mode 100644 index 0000000000..b37d846482 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart @@ -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; + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart index 8f6ac6d6ee..e118f5ea62 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/selection_service.dart @@ -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 on State { + /// + List 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 on State { /// 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 EditorState get editorState => widget.editorState; + Node? _selectedNodeInPostion(Node node, Position position) => + node.childAtPath(position.path); + + @override + List currentSelectedNodes = []; + @override List getNodesInSelection(Selection selection) => _selectedNodesInSelection(editorState.document.root, selection); @@ -129,16 +140,21 @@ class _FlowySelectionState extends State @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 getNodesInRange(Offset start, [Offset? end]) { if (end != null) { @@ -172,7 +188,7 @@ class _FlowySelectionState extends State List computeNodesInRange(Node node, Offset start, Offset end) { List 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 } @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 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 // 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(); - shortcutService?.hide(); + // clear floating shortcusts + editorState.service.floatingShortcutServiceKey.currentState + ?.unwrapOrNull() + ?.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 } 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 } 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 List _selectedNodesInSelection(Node node, Selection selection) { List result = []; if (node.parent != null) { - if (_isNodeInSelection(node, selection)) { + if (node.inSelection(selection)) { result.add(node); } } @@ -374,12 +415,4 @@ class _FlowySelectionState extends State } 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); - } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index d272364b44..16ccadb079 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -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', () {