From 190728453476b64a6ffc3dd89c147b9dd240d5af Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 26 Jul 2022 13:32:22 +0800 Subject: [PATCH 1/5] chore: add selection_service documentation and resymbol shortcut service. --- .../flowy_editor/example/lib/main.dart | 2 +- .../lib/extensions/node_extensions.dart | 11 +++ .../lib/service/editor_service.dart | 20 +++-- .../arrow_keys_handler.dart | 0 .../delete_nodes_handler.dart | 0 .../delete_single_text_node_handler.dart | 0 .../shortcut_handler.dart | 0 .../lib/service/selection_service.dart | 88 ++++++++++--------- .../flowy_editor/lib/service/service.dart | 2 +- ...cut_service.dart => shortcut_service.dart} | 0 10 files changed, 70 insertions(+), 53 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart rename frontend/app_flowy/packages/flowy_editor/lib/service/{flowy_key_event_handlers => internal_key_event_handlers}/arrow_keys_handler.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/service/{flowy_key_event_handlers => internal_key_event_handlers}/delete_nodes_handler.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/service/{flowy_key_event_handlers => internal_key_event_handlers}/delete_single_text_node_handler.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/service/{flowy_key_event_handlers => internal_key_event_handlers}/shortcut_handler.dart (100%) rename frontend/app_flowy/packages/flowy_editor/lib/service/{floating_shortcut_service.dart => shortcut_service.dart} (100%) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart index 9cc4d8c536..112c1dcd4f 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -96,7 +96,7 @@ class _MyHomePageState extends State { ); return FlowyEditor( editorState: _editorState, - keyEventHandler: const [], + keyEventHandlers: const [], shortcuts: [ // TODO: this won't work, just a example for now. { 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 new file mode 100644 index 0000000000..35cc18cdd2 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart @@ -0,0 +1,11 @@ +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/extensions/object_extensions.dart'; +import 'package:flowy_editor/render/selection/selectable.dart'; +import 'package:flutter/material.dart'; + +extension NodeExtensions on Node { + RenderBox? get renderBox => + key?.currentContext?.findRenderObject()?.unwrapOrNull(); + + Selectable? get selectable => key?.currentState?.unwrapOrNull(); +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart index 7cd4eaf708..8b40981ccb 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/editor_service.dart @@ -1,25 +1,27 @@ import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart'; -import 'package:flowy_editor/service/floating_shortcut_service.dart'; -import 'package:flowy_editor/service/flowy_key_event_handlers/arrow_keys_handler.dart'; -import 'package:flowy_editor/service/flowy_key_event_handlers/delete_nodes_handler.dart'; -import 'package:flowy_editor/service/flowy_key_event_handlers/delete_single_text_node_handler.dart'; -import 'package:flowy_editor/service/flowy_key_event_handlers/shortcut_handler.dart'; +import 'package:flowy_editor/service/shortcut_service.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/delete_single_text_node_handler.dart'; +import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; +import 'package:flowy_editor/editor_state.dart'; -import '../editor_state.dart'; import 'package:flutter/material.dart'; class FlowyEditor extends StatefulWidget { const FlowyEditor({ Key? key, required this.editorState, - required this.keyEventHandler, + required this.keyEventHandlers, required this.shortcuts, }) : super(key: key); final EditorState editorState; - final List keyEventHandler; + final List keyEventHandlers; + + /// Shortcusts final FloatingShortcuts shortcuts; @override @@ -41,7 +43,7 @@ class _FlowyEditorState extends State { flowyDeleteNodesHandler, deleteSingleTextNodeHandler, arrowKeysHandler, - ...widget.keyEventHandler, + ...widget.keyEventHandlers, ], editorState: editorState, child: FloatingShortcut( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_nodes_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_nodes_handler.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_nodes_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_nodes_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/delete_single_text_node_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/shortcut_handler.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart 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 2f4bba86ec..fa71536ecc 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,7 +1,8 @@ import 'package:flowy_editor/render/selection/cursor_widget.dart'; import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; -import 'package:flowy_editor/service/floating_shortcut_service.dart'; +import 'package:flowy_editor/extensions/node_extensions.dart'; +import 'package:flowy_editor/service/shortcut_service.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -11,58 +12,65 @@ import '../render/selection/selectable.dart'; /// Process selection and cursor mixin FlowySelectionService on State { - /// [Pan] and [Tap] must be mutually exclusive. - /// Pan - Offset? panStartOffset; - Offset? panEndOffset; - - /// Tap - Offset? tapOffset; - + /// [start] and [end] are the offsets under the global coordinate system. void updateSelection(Offset start, Offset end); + /// [start] is the offset under the global coordinate system. void updateCursor(Offset start); - /// Returns selected node(s) - /// Returns empty list if no nodes are being selected. + /// Returns selected [Node]s. Empty list would be returned + /// if no nodes are being selected. + /// + /// + /// [start] and [end] are the offsets under the global coordinate system. + /// + /// If end is not null, it means multiple selection, + /// otherwise single selection. List getSelectedNodes(Offset start, [Offset? end]); - /// Compute selected node triggered by [Tap] - Node? computeSelectedNodeInOffset( - Node node, - Offset offset, - ); + /// Return the [Node] or [Null] in single selection. + /// + /// [start] is the offset under the global coordinate system. + Node? computeSelectedNodeInOffset(Node node, Offset offset); - /// Compute selected nodes triggered by [Pan] + /// Return the [Node]s in multiple selection. Emtpy list would be returned + /// if no nodes are in range. + /// + /// [start] is the offset under the global coordinate system. List computeSelectedNodesInRange( Node node, Offset start, Offset end, ); - /// Pan + /// Return [bool] to identify the [Node] is in Range or not. + /// + /// [start] and [end] are the offsets under the global coordinate system. bool isNodeInSelection( Node node, Offset start, Offset end, ); - /// Tap - bool isNodeInOffset( - Node node, - Offset offset, - ); + /// Return [bool] to identify the [Node] contains [Offset] or not. + /// + /// [start] is the offset under the global coordinate system. + bool isNodeInOffset(Node node, Offset offset); } class FlowySelection extends StatefulWidget { const FlowySelection({ Key? key, + this.cursorColor = Colors.black, + this.selectionColor = const Color.fromARGB(60, 61, 61, 213), required this.editorState, required this.child, }) : super(key: key); final EditorState editorState; final Widget child; + final Color cursorColor; + final Color selectionColor; @override State createState() => _FlowySelectionState(); @@ -75,6 +83,14 @@ class _FlowySelectionState extends State final List _selectionOverlays = []; final List _cursorOverlays = []; + /// [Pan] and [Tap] must be mutually exclusive. + /// Pan + Offset? panStartOffset; + Offset? panEndOffset; + + /// Tap + Offset? tapOffset; + EditorState get editorState => widget.editorState; @override @@ -123,7 +139,7 @@ class _FlowySelectionState extends State for (final rect in selectionRects) { final overlay = OverlayEntry( builder: ((context) => SelectionWidget( - color: Colors.yellow.withAlpha(100), + color: widget.selectionColor, layerLink: node.layerLink, rect: rect, )), @@ -154,7 +170,7 @@ class _FlowySelectionState extends State builder: ((context) => CursorWidget( key: _cursorKey, rect: rect, - color: Colors.red, + color: widget.cursorColor, layerLink: selectedNode.layerLink, )), ); @@ -165,16 +181,10 @@ class _FlowySelectionState extends State @override List getSelectedNodes(Offset start, [Offset? end]) { if (end != null) { - return computeSelectedNodesInRange( - editorState.document.root, - start, - end, - ); + return computeSelectedNodesInRange(editorState.document.root, start, end); } else { - final reuslt = computeSelectedNodeInOffset( - editorState.document.root, - start, - ); + final reuslt = + computeSelectedNodeInOffset(editorState.document.root, start); if (reuslt != null) { return [reuslt]; } @@ -190,13 +200,11 @@ class _FlowySelectionState extends State return result; } } - if (node.parent != null && node.key != null) { if (isNodeInOffset(node, offset)) { return node; } } - return null; } @@ -217,9 +225,7 @@ class _FlowySelectionState extends State @override bool isNodeInOffset(Node node, Offset offset) { - assert(node.key != null); - final renderBox = - node.key?.currentContext?.findRenderObject() as RenderBox?; + final renderBox = node.renderBox; if (renderBox != null) { final boxOffset = renderBox.localToGlobal(Offset.zero); final boxRect = boxOffset & renderBox.size; @@ -230,9 +236,7 @@ class _FlowySelectionState extends State @override bool isNodeInSelection(Node node, Offset start, Offset end) { - assert(node.key != null); - final renderBox = - node.key?.currentContext?.findRenderObject() as RenderBox?; + final renderBox = node.renderBox; if (renderBox != null) { final rect = Rect.fromPoints(start, end); final boxOffset = renderBox.localToGlobal(Offset.zero); diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart index 7833e6d379..f8cf4a9e5c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/service.dart @@ -1,4 +1,4 @@ -import 'package:flowy_editor/service/floating_shortcut_service.dart'; +import 'package:flowy_editor/service/shortcut_service.dart'; import 'package:flowy_editor/service/selection_service.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/shortcut_service.dart similarity index 100% rename from frontend/app_flowy/packages/flowy_editor/lib/service/floating_shortcut_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/shortcut_service.dart From 941671568e40f0cf87f10e3432ca45e3674f5f8a Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 26 Jul 2022 14:31:10 +0800 Subject: [PATCH 2/5] chore: remove unused import --- .../flowy_editor/example/lib/plugin/text_node_widget.dart | 1 - 1 file changed, 1 deletion(-) 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 cfb0ea5383..42bfd1e9f1 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 @@ -1,6 +1,5 @@ import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/selection.dart'; -import 'package:flowy_editor/document/text_delta.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flowy_editor/flowy_editor.dart'; From 114ae2b45dd7b7b4cf42bd8ac11df1e188f5cfa1 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 26 Jul 2022 18:40:53 +0800 Subject: [PATCH 3/5] feat: compute cursor and selection by [Selection] or [Offset] --- .../example/lib/plugin/image_node_widget.dart | 26 +- .../lib/plugin/selected_text_node_widget.dart | 65 +++-- .../flowy_editor/lib/document/path.dart | 26 ++ .../flowy_editor/lib/document/position.dart | 7 + .../flowy_editor/lib/document/selection.dart | 23 +- .../lib/render/selection/selectable.dart | 13 +- .../arrow_keys_handler.dart | 2 +- .../delete_single_text_node_handler.dart | 4 +- .../shortcut_handler.dart | 14 +- .../lib/service/selection_service.dart | 230 ++++++++++++------ 10 files changed, 270 insertions(+), 140 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 8a9c96b22e..fc440a8fa5 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -1,3 +1,5 @@ +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/material.dart'; @@ -38,27 +40,27 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { String get src => widget.node.attributes['image_src'] as String; @override - List getSelectionRectsInRange(Offset start, Offset end) { - final renderBox = context.findRenderObject() as RenderBox; - return [Offset.zero & renderBox.size]; + List getRectsInSelection(Selection selection) { + // TODO: implement getRectsInSelection + throw UnimplementedError(); } @override - Rect getCursorRect(Offset start) { - final renderBox = context.findRenderObject() as RenderBox; - final size = Size(2, renderBox.size.height); - final cursorOffset = Offset(renderBox.size.width, 0); - return cursorOffset & size; + Selection getSelectionInRange(Offset start, Offset end) { + // TODO: implement getSelectionInRange + throw UnimplementedError(); } @override - TextSelection? getCurrentTextSelection() { - return null; + Rect getCursorRectInPosition(Position position) { + // TODO: implement getCursorRectInPosition + throw UnimplementedError(); } @override - Offset getOffsetByTextSelection(TextSelection textSelection) { - return Offset.zero; + Position getPositionInOffset(Offset start) { + // TODO: implement getPositionInOffset + throw UnimplementedError(); } @override diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index b234ecd967..0f20f2fe3d 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -1,6 +1,8 @@ import 'dart:math'; import 'package:example/plugin/debuggable_rich_text.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/flowy_editor.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -56,49 +58,43 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> _textKey.currentContext?.findRenderObject() as RenderParagraph; @override - List getSelectionRectsInRange(Offset start, Offset end) { + Selection getSelectionInRange(Offset start, Offset end) { final localStart = _renderParagraph.globalToLocal(start); final localEnd = _renderParagraph.globalToLocal(end); - - var textSelection = - TextSelection(baseOffset: 0, extentOffset: node.toRawString().length); - // Returns select all if the start or end exceeds the size of the box - // TODO: don't need to compute everytime. - var rects = _computeSelectionRects(textSelection); - _textSelection = textSelection; - - if (localEnd.dy > localStart.dy) { - // downward - if (localEnd.dy >= rects.last.bottom) { - return rects; - } - } else { - // upward - if (localEnd.dy <= rects.first.top) { - return rects; - } - } - - final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset; - final selectionExtentOffset = _getTextPositionAtOffset(localEnd).offset; - textSelection = TextSelection( - baseOffset: selectionBaseOffset, - extentOffset: selectionExtentOffset, + final baseOffset = _getTextPositionAtOffset(localStart).offset; + final extentOffset = _getTextPositionAtOffset(localEnd).offset; + return Selection.single( + path: node.path, + startOffset: baseOffset, + endOffset: extentOffset, + ); + } + + @override + List getRectsInSelection(Selection selection) { + assert(pathEquals(selection.start.path, selection.end.path)); + assert(pathEquals(selection.start.path, node.path)); + final textSelection = TextSelection( + baseOffset: selection.start.offset, + extentOffset: selection.end.offset, ); - _textSelection = textSelection; return _computeSelectionRects(textSelection); } @override - Rect getCursorRect(Offset start) { - final localStart = _renderParagraph.globalToLocal(start); - final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset; - final textSelection = TextSelection.collapsed(offset: selectionBaseOffset); + Rect getCursorRectInPosition(Position position) { + final textSelection = TextSelection.collapsed(offset: position.offset); _textSelection = textSelection; - print('text selection = $textSelection'); return _computeCursorRect(textSelection.baseOffset); } + @override + Position getPositionInOffset(Offset start) { + final localStart = _renderParagraph.globalToLocal(start); + final baseOffset = _getTextPositionAtOffset(localStart).offset; + return Position(path: node.path, offset: baseOffset); + } + @override TextSelection? getCurrentTextSelection() { return _textSelection; @@ -175,8 +171,8 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> return _renderParagraph.getPositionForOffset(offset); } - List _computeSelectionRects(TextSelection selection) { - final textBoxes = _renderParagraph.getBoxesForSelection(selection); + List _computeSelectionRects(TextSelection textSelection) { + final textBoxes = _renderParagraph.getBoxesForSelection(textSelection); return textBoxes.map((box) => box.toRect()).toList(); } @@ -185,7 +181,6 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> final cursorOffset = _renderParagraph.getOffsetForCaret(position, Rect.zero); final cursorHeight = _renderParagraph.getFullHeightForCaret(position); - print('offset = $offset, cursorHeight = $cursorHeight'); if (cursorHeight != null) { const cursorWidth = 2; return Rect.fromLTWH( 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 a8163f094d..bef96a7bd2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/foundation.dart'; typedef Path = List; @@ -5,3 +7,27 @@ 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 88941cd82e..e213c1eb33 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart @@ -24,4 +24,11 @@ class Position { final pathHash = hashList(path); return Object.hash(pathHash, offset); } + + Position copyWith({Path? path, int? offset}) { + return Position( + path: path ?? this.path, + offset: offset ?? this.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 dea3a2b752..fe60e1abec 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart @@ -1,4 +1,5 @@ -import './position.dart'; +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/position.dart'; class Selection { final Position start; @@ -9,9 +10,16 @@ class Selection { required this.end, }); - factory Selection.collapsed(Position pos) { - return Selection(start: pos, end: pos); - } + Selection.single({ + required Path path, + required int startOffset, + int? endOffset, + }) : start = Position(path: path, offset: startOffset), + end = Position(path: path, offset: endOffset ?? startOffset); + + Selection.collapsed(Position position) + : start = position, + end = position; Selection collapse({bool atStart = false}) { if (atStart) { @@ -24,4 +32,11 @@ class Selection { bool isCollapsed() { return start == end; } + + Selection copyWith({Position? start, Position? end}) { + return Selection( + start: start ?? this.start, + end: end ?? this.end, + ); + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 6fc51049a1..f94d07e457 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -1,3 +1,5 @@ +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flutter/material.dart'; /// @@ -9,14 +11,17 @@ mixin Selectable on State { /// /// The return result must be a [List] of the [Rect] /// under the local coordinate system. - List getSelectionRectsInRange(Offset start, Offset end); + Selection getSelectionInRange(Offset start, Offset end); + + List getRectsInSelection(Selection selection); /// Returns a [Rect] for the offset in current widget. /// /// [start] is the offset of the global coordination system. /// /// The return result must be an offset of the local coordinate system. - Rect getCursorRect(Offset start); + Position getPositionInOffset(Offset start); + Rect getCursorRectInPosition(Position position); /// Returns a backward offset of the current offset based on the cause. Offset getBackwardOffset(/* Cause */); @@ -30,12 +35,12 @@ mixin Selectable on State { /// /// Only the widget rendered by [TextNode] need to implement the detail, /// and the rest can return null. - TextSelection? getCurrentTextSelection(); + TextSelection? getCurrentTextSelection() => null; /// For [TextNode] only. /// /// Retruns a [Offset]. /// Only the widget rendered by [TextNode] need to implement the detail, /// and the rest can return [Offset.zero]. - Offset getOffsetByTextSelection(TextSelection textSelection); + Offset getOffsetByTextSelection(TextSelection textSelection) => Offset.zero; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index 3049f54453..44fc9a146f 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -30,7 +30,7 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { } final selectionService = editorState.service.selectionService; if (offset != null) { - selectionService.updateCursor(offset); + // selectionService.updateCursor(offset); return KeyEventResult.handled; } return KeyEventResult.ignored; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart index db12d2bbb2..47a83f314a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/delete_single_text_node_handler.dart @@ -37,7 +37,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { final newOfset = previousSelectable ?.getOffsetByTextSelection(newTextSelection); if (newOfset != null) { - selectionService.updateCursor(newOfset); + // selectionService.updateCursor(newOfset); } // merge TransactionBuilder(editorState) @@ -58,7 +58,7 @@ FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) { final selectionService = editorState.service.selectionService; final newOfset = selectable.getOffsetByTextSelection(newTextSelection); - selectionService.updateCursor(newOfset); + // selectionService.updateCursor(newOfset); return KeyEventResult.handled; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart index 4e52d1bbe9..3eef8c1d1b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/shortcut_handler.dart @@ -18,13 +18,13 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) { final textNode = selectedNodes.first.unwrapOrNull(); final selectable = textNode?.key?.currentState?.unwrapOrNull(); final textSelection = selectable?.getCurrentTextSelection(); - if (textNode != null && selectable != null && textSelection != null) { - final offset = selectable.getOffsetByTextSelection(textSelection); - final rect = selectable.getCursorRect(offset); - editorState.service.floatingToolbarService - .showInOffset(rect.topLeft, textNode.layerLink); - return KeyEventResult.handled; - } + // if (textNode != null && selectable != null && textSelection != null) { + // final offset = selectable.getOffsetByTextSelection(textSelection); + // final rect = selectable.getCursorRect(offset); + // editorState.service.floatingToolbarService + // .showInOffset(rect.topLeft, textNode.layerLink); + // return KeyEventResult.handled; + // } return KeyEventResult.ignored; }; 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 fa71536ecc..8f6ac6d6ee 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,3 +1,6 @@ +import 'package:flowy_editor/document/path.dart'; +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/render/selection/cursor_widget.dart'; import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; import 'package:flowy_editor/extensions/object_extensions.dart'; @@ -12,11 +15,8 @@ import '../render/selection/selectable.dart'; /// Process selection and cursor mixin FlowySelectionService on State { - /// [start] and [end] are the offsets under the global coordinate system. - void updateSelection(Offset start, Offset end); - - /// [start] is the offset under the global coordinate system. - void updateCursor(Offset start); + /// + void updateSelection(Selection selection); /// Returns selected [Node]s. Empty list would be returned /// if no nodes are being selected. @@ -26,18 +26,21 @@ mixin FlowySelectionService on State { /// /// If end is not null, it means multiple selection, /// otherwise single selection. - List getSelectedNodes(Offset start, [Offset? end]); + List getNodesInRange(Offset start, [Offset? end]); + + /// + List getNodesInSelection(Selection selection); /// Return the [Node] or [Null] in single selection. /// /// [start] is the offset under the global coordinate system. - Node? computeSelectedNodeInOffset(Node node, Offset offset); + Node? computeNodeInOffset(Node node, Offset offset); /// Return the [Node]s in multiple selection. Emtpy list would be returned /// if no nodes are in range. /// /// [start] is the offset under the global coordinate system. - List computeSelectedNodesInRange( + List computeNodesInRange( Node node, Offset start, Offset end, @@ -93,6 +96,10 @@ class _FlowySelectionState extends State EditorState get editorState => widget.editorState; + @override + List getNodesInSelection(Selection selection) => + _selectedNodesInSelection(editorState.document.root, selection); + @override Widget build(BuildContext context) { return RawGestureDetector( @@ -121,70 +128,23 @@ class _FlowySelectionState extends State } @override - void updateSelection(Offset start, Offset end) { + void updateSelection(Selection selection) { _clearAllOverlayEntries(); - final nodes = getSelectedNodes(start, end); - editorState.selectedNodes = nodes; - if (nodes.isEmpty) { - return; - } - - for (final node in nodes) { - if (node.key?.currentState is! Selectable) { - continue; - } - final selectable = node.key?.currentState as Selectable; - final selectionRects = selectable.getSelectionRectsInRange(start, end); - for (final rect in selectionRects) { - final overlay = OverlayEntry( - builder: ((context) => SelectionWidget( - color: widget.selectionColor, - layerLink: node.layerLink, - rect: rect, - )), - ); - _selectionOverlays.add(overlay); - } - } - Overlay.of(context)?.insertAll(_selectionOverlays); - } - - @override - void updateCursor(Offset start) { - _clearAllOverlayEntries(); - - final nodes = getSelectedNodes(start); - editorState.selectedNodes = nodes; - if (nodes.isEmpty) { - return; - } - - final selectedNode = nodes.first; - if (selectedNode.key?.currentState is! Selectable) { - return; - } - final selectable = selectedNode.key?.currentState as Selectable; - final rect = selectable.getCursorRect(start); - final cursor = OverlayEntry( - builder: ((context) => CursorWidget( - key: _cursorKey, - rect: rect, - color: widget.cursorColor, - layerLink: selectedNode.layerLink, - )), - ); - _cursorOverlays.add(cursor); - Overlay.of(context)?.insertAll(_cursorOverlays); - } - - @override - List getSelectedNodes(Offset start, [Offset? end]) { - if (end != null) { - return computeSelectedNodesInRange(editorState.document.root, start, end); + // cursor + if (selection.isCollapsed()) { + _updateCursor(selection.start); } else { - final reuslt = - computeSelectedNodeInOffset(editorState.document.root, start); + _updateSelection(selection); + } + } + + @override + List getNodesInRange(Offset start, [Offset? end]) { + if (end != null) { + return computeNodesInRange(editorState.document.root, start, end); + } else { + final reuslt = computeNodeInOffset(editorState.document.root, start); if (reuslt != null) { return [reuslt]; } @@ -193,9 +153,9 @@ class _FlowySelectionState extends State } @override - Node? computeSelectedNodeInOffset(Node node, Offset offset) { + Node? computeNodeInOffset(Node node, Offset offset) { for (final child in node.children) { - final result = computeSelectedNodeInOffset(child, offset); + final result = computeNodeInOffset(child, offset); if (result != null) { return result; } @@ -209,7 +169,7 @@ class _FlowySelectionState extends State } @override - List computeSelectedNodesInRange(Node node, Offset start, Offset end) { + List computeNodesInRange(Node node, Offset start, Offset end) { List result = []; if (node.parent != null && node.key != null) { if (isNodeInSelection(node, start, end)) { @@ -217,7 +177,7 @@ class _FlowySelectionState extends State } } for (final child in node.children) { - result.addAll(computeSelectedNodesInRange(child, start, end)); + result.addAll(computeNodesInRange(child, start, end)); } // TODO: sort the result return result; @@ -254,7 +214,16 @@ class _FlowySelectionState extends State panStartOffset = null; panEndOffset = null; - updateCursor(tapOffset!); + final nodes = getNodesInRange(tapOffset!); + if (nodes.isNotEmpty) { + assert(nodes.length == 1); + final selectable = nodes.first.selectable; + if (selectable != null) { + final position = selectable.getPositionInOffset(tapOffset!); + final selection = Selection.collapsed(position); + updateSelection(selection); + } + } } void _onPanStart(DragStartDetails details) { @@ -271,7 +240,16 @@ class _FlowySelectionState extends State panEndOffset = details.globalPosition; tapOffset = null; - updateSelection(panStartOffset!, panEndOffset!); + final nodes = getNodesInRange(panStartOffset!, panEndOffset!); + 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, + ); + updateSelection(selection); + } } void _onPanEnd(DragEndDetails details) { @@ -302,4 +280,106 @@ class _FlowySelectionState extends State ?.unwrapOrNull(); shortcutService?.hide(); } + + void _updateSelection(Selection selection) { + final nodes = + _selectedNodesInSelection(editorState.document.root, selection); + + var index = 0; + for (final node in nodes) { + final selectable = node.selectable; + if (selectable == null) { + continue; + } + + Selection newSelection; + 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), + ); + } else if (index == nodes.length - 1) { + newSelection = selection.copyWith( + /// FIXME: make it better. + start: selection.end.copyWith(offset: 0), + ); + } else { + final position = Position(path: node.path); + newSelection = Selection( + start: position.copyWith(offset: 0), + end: position.copyWith(offset: node.toRawString().length), + ); + } + } + } else { + newSelection = Selection.collapsed( + Position(path: node.path), + ); + } + + final rects = selectable.getRectsInSelection(newSelection); + + for (final rect in rects) { + final overlay = OverlayEntry( + builder: ((context) => SelectionWidget( + color: widget.selectionColor, + layerLink: node.layerLink, + rect: rect, + )), + ); + _selectionOverlays.add(overlay); + } + index += 1; + } + Overlay.of(context)?.insertAll(_selectionOverlays); + } + + void _updateCursor(Position position) { + final node = _selectedNodeInPostion(editorState.document.root, position); + + assert(node != null); + if (node == null) { + return; + } + + final selectable = node.selectable; + final rect = selectable?.getCursorRectInPosition(position); + if (rect != null) { + final cursor = OverlayEntry( + builder: ((context) => CursorWidget( + key: _cursorKey, + rect: rect, + color: widget.cursorColor, + layerLink: node.layerLink, + )), + ); + _cursorOverlays.add(cursor); + Overlay.of(context)?.insertAll(_cursorOverlays); + } + } + + List _selectedNodesInSelection(Node node, Selection selection) { + List result = []; + if (node.parent != null) { + if (_isNodeInSelection(node, selection)) { + result.add(node); + } + } + for (final child in node.children) { + result.addAll(_selectedNodesInSelection(child, selection)); + } + 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); + } } From cde2127dec91c473eb42bfd9e172fc7817c18e05 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 26 Jul 2022 20:10:47 +0800 Subject: [PATCH 4/5] feat: compute cursor and selection by [Selection] or [Offset] --- .../example/lib/plugin/text_node_widget.dart | 2 +- .../flowy_editor/lib/document/path.dart | 24 ---- .../flowy_editor/lib/document/position.dart | 3 + .../flowy_editor/lib/document/selection.dart | 12 +- .../lib/extensions/node_extensions.dart | 12 ++ .../lib/extensions/path_extensions.dart | 25 ++++ .../lib/service/selection_service.dart | 127 +++++++++++------- .../flowy_editor/test/flowy_editor_test.dart | 2 +- 8 files changed, 131 insertions(+), 76 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/extensions/path_extensions.dart 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', () { From c048c8f623451ea7fbbb4499b90846104826420c Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 26 Jul 2022 23:28:51 +0800 Subject: [PATCH 5/5] fix: compute wrong upward selection --- .../example/lib/plugin/image_node_widget.dart | 22 +-- .../lib/plugin/selected_text_node_widget.dart | 23 +-- .../flowy_editor/lib/document/selection.dart | 3 + .../lib/render/selection/selectable.dart | 7 +- .../arrow_keys_handler.dart | 23 --- .../lib/service/selection_service.dart | 137 ++++++++---------- 6 files changed, 83 insertions(+), 132 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index fc440a8fa5..c5084df2fb 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart @@ -39,6 +39,18 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { EditorState get editorState => widget.editorState; String get src => widget.node.attributes['image_src'] as String; + @override + Position end() { + // TODO: implement end + throw UnimplementedError(); + } + + @override + Position start() { + // TODO: implement start + throw UnimplementedError(); + } + @override List getRectsInSelection(Selection selection) { // TODO: implement getRectsInSelection @@ -63,16 +75,6 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { throw UnimplementedError(); } - @override - Offset getBackwardOffset() { - return Offset.zero; - } - - @override - Offset getForwardOffset() { - return Offset.zero; - } - @override Widget build(BuildContext context) { return _build(context); diff --git a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart index 0f20f2fe3d..894f6b1848 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/selected_text_node_widget.dart @@ -107,28 +107,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> } @override - Offset getBackwardOffset() { - final textSelection = _textSelection; - if (textSelection != null) { - final leftTextSelection = TextSelection.collapsed( - offset: max(0, textSelection.baseOffset - 1), - ); - return getOffsetByTextSelection(leftTextSelection); - } - return Offset.zero; - } + Position start() => Position(path: node.path, offset: 0); @override - Offset getForwardOffset() { - final textSelection = _textSelection; - if (textSelection != null) { - final leftTextSelection = TextSelection.collapsed( - offset: min(node.toRawString().length, textSelection.extentOffset + 1), - ); - return getOffsetByTextSelection(leftTextSelection); - } - return Offset.zero; - } + Position end() => + Position(path: node.path, offset: node.toRawString().length); @override Widget build(BuildContext context) { 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 1734fabf24..a3919a21f6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart @@ -31,6 +31,7 @@ class Selection { } bool get isCollapsed => start == end; + bool get isSingle => pathEquals(start.path, end.path); bool get isUpward => start.path >= end.path && !pathEquals(start.path, end.path); bool get isDownward => @@ -43,6 +44,8 @@ class Selection { ); } + Selection copy() => Selection(start: start, end: end); + @override String toString() => '[Selection] start = $start, end = $end'; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index f94d07e457..4d155972df 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -23,11 +23,8 @@ mixin Selectable on State { Position getPositionInOffset(Offset start); Rect getCursorRectInPosition(Position position); - /// Returns a backward offset of the current offset based on the cause. - Offset getBackwardOffset(/* Cause */); - - /// Returns a forward offset of the current offset based on the cause. - Offset getForwardOffset(/* Cause */); + Position start(); + Position end(); /// For [TextNode] only. /// diff --git a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart index 44fc9a146f..95496db2ea 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -1,5 +1,3 @@ -import 'package:flowy_editor/extensions/object_extensions.dart'; -import 'package:flowy_editor/flowy_editor.dart'; import 'package:flowy_editor/service/keyboard_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,26 +10,5 @@ FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { return KeyEventResult.ignored; } - // TODO: Up and Down - - // Left and Right - final selectedNodes = editorState.selectedNodes; - if (selectedNodes.length != 1) { - return KeyEventResult.ignored; - } - - final node = selectedNodes.first.unwrapOrNull(); - final selectable = node?.key?.currentState?.unwrapOrNull(); - Offset? offset; - if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - offset = selectable?.getBackwardOffset(); - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - offset = selectable?.getForwardOffset(); - } - final selectionService = editorState.service.selectionService; - if (offset != null) { - // selectionService.updateCursor(offset); - return KeyEventResult.handled; - } return KeyEventResult.ignored; }; 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 e118f5ea62..19604b0227 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 @@ -14,15 +14,26 @@ import 'package:flutter/material.dart'; /// Process selection and cursor mixin FlowySelectionService on State { + /// Returns the currently selected [Node]s. /// + /// The order of the return is determined according to the selected order. List get currentSelectedNodes; + /// ------------------ Selection ------------------------ + /// void updateSelection(Selection selection); /// void clearSelection(); + /// + List getNodesInSelection(Selection selection); + + /// ------------------ Selection ------------------------ + + /// ------------------ Offset ------------------------ + /// Returns selected [Node]s. Empty list would be returned /// if no nodes are being selected. /// @@ -33,9 +44,6 @@ mixin FlowySelectionService on State { /// otherwise single selection. List getNodesInRange(Offset start, [Offset? end]); - /// - List getNodesInSelection(Selection selection); - /// Return the [Node] or [Null] in single selection. /// /// [start] is the offset under the global coordinate system. @@ -64,6 +72,8 @@ mixin FlowySelectionService on State { /// /// [start] is the offset under the global coordinate system. bool isNodeInOffset(Node node, Offset offset); + + /// ------------------ Offset ------------------------ } class FlowySelection extends StatefulWidget { @@ -101,9 +111,6 @@ class _FlowySelectionState extends State EditorState get editorState => widget.editorState; - Node? _selectedNodeInPostion(Node node, Position position) => - node.childAtPath(position.path); - @override List currentSelectedNodes = []; @@ -186,6 +193,17 @@ class _FlowySelectionState extends State @override List computeNodesInRange(Node node, Offset start, Offset end) { + final result = _computeNodesInRange(node, start, end); + if (start.dy <= end.dy) { + // downward + return result; + } else { + // upward + return result.reversed.toList(growable: false); + } + } + + List _computeNodesInRange(Node node, Offset start, Offset end) { List result = []; if (node.parent != null && node.key != null) { if (isNodeInRange(node, start, end)) { @@ -195,7 +213,6 @@ class _FlowySelectionState extends State for (final child in node.children) { result.addAll(computeNodesInRange(child, start, end)); } - // TODO: sort the result return result; } @@ -223,13 +240,12 @@ class _FlowySelectionState extends State } void _onTapDown(TapDownDetails details) { - debugPrint('on tap down'); - - // TODO: use setter to make them exclusive?? - tapOffset = details.globalPosition; + // clear old state. panStartOffset = null; panEndOffset = null; + tapOffset = details.globalPosition; + final nodes = getNodesInRange(tapOffset!); if (nodes.isNotEmpty) { assert(nodes.length == 1); @@ -243,38 +259,30 @@ class _FlowySelectionState extends State } void _onPanStart(DragStartDetails details) { - debugPrint('on pan start'); - - panStartOffset = details.globalPosition; + // clear old state. panEndOffset = null; tapOffset = null; + clearSelection(); + + panStartOffset = details.globalPosition; } void _onPanUpdate(DragUpdateDetails details) { - // debugPrint('on pan update'); - panEndOffset = details.globalPosition; - tapOffset = null; final nodes = getNodesInRange(panStartOffset!, panEndOffset!); final first = nodes.first.selectable; final last = nodes.last.selectable; + + // compute the selection in range. if (first != null && last != null) { - 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, - ); - } + bool isDownward = panStartOffset!.dy <= panEndOffset!.dy; + final start = + first.getSelectionInRange(panStartOffset!, panEndOffset!).start; + final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end; + final selection = Selection( + start: isDownward ? start : end, end: isDownward ? end : start); + debugPrint('[_onPanUpdate] $selection'); updateSelection(selection); } } @@ -313,51 +321,32 @@ class _FlowySelectionState extends State continue; } - 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) { - 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) { - 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), - ); - } + var newSelection = selection.copy(); + // In the case of multiple selections, + // we need to return a new selection for each selected node individually. + if (!selection.isSingle) { + // <> means selected. + // text: abcdopqr + if (index == 0) { + if (selection.isDownward) { + newSelection = selection.copyWith(end: selectable.end()); } else { - final position = Position(path: node.path); - newSelection = Selection( - start: position.copyWith(offset: 0), - end: position.copyWith(offset: node.toRawString().length), - ); + newSelection = selection.copyWith(start: selectable.start()); } + } else if (index == nodes.length - 1) { + if (selection.isDownward) { + newSelection = selection.copyWith(start: selectable.start()); + } else { + newSelection = selection.copyWith(end: selectable.end()); + } + } else { + newSelection = selection.copyWith( + start: selectable.start(), + end: selectable.end(), + ); } - } else { - newSelection = Selection.collapsed( - Position(path: node.path), - ); } final rects = selectable.getRectsInSelection(newSelection);