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/example/lib/plugin/image_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/image_node_widget.dart index 8a9c96b22e..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 @@ -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,37 +40,39 @@ 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]; + Position end() { + // TODO: implement end + 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; + Position start() { + // TODO: implement start + throw UnimplementedError(); } @override - TextSelection? getCurrentTextSelection() { - return null; + List getRectsInSelection(Selection selection) { + // TODO: implement getRectsInSelection + throw UnimplementedError(); } @override - Offset getOffsetByTextSelection(TextSelection textSelection) { - return Offset.zero; + Selection getSelectionInRange(Offset start, Offset end) { + // TODO: implement getSelectionInRange + throw UnimplementedError(); } @override - Offset getBackwardOffset() { - return Offset.zero; + Rect getCursorRectInPosition(Position position) { + // TODO: implement getCursorRectInPosition + throw UnimplementedError(); } @override - Offset getForwardOffset() { - 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..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 @@ -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; @@ -111,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) { @@ -175,8 +154,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 +164,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/example/lib/plugin/text_node_widget.dart b/frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_node_widget.dart index cfb0ea5383..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 @@ -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'; @@ -327,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 a8163f094d..8f24947649 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; 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..a60f04e89b 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,14 @@ 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, + ); + } + + @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 dea3a2b752..a3919a21f6 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,6 @@ -import './position.dart'; +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; @@ -9,9 +11,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) { @@ -21,7 +30,22 @@ class Selection { } } - bool isCollapsed() { - return start == end; + 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 => + start.path <= end.path && !pathEquals(start.path, end.path); + + Selection copyWith({Position? start, Position? end}) { + return Selection( + start: start ?? this.start, + end: end ?? this.end, + ); } + + 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/extensions/node_extensions.dart b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart new file mode 100644 index 0000000000..49cc38f749 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/extensions/node_extensions.dart @@ -0,0 +1,23 @@ +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'; + +extension NodeExtensions on Node { + RenderBox? get renderBox => + 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/render/selection/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 6fc51049a1..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 @@ -1,3 +1,5 @@ +import 'package:flowy_editor/document/position.dart'; +import 'package:flowy_editor/document/selection.dart'; import 'package:flutter/material.dart'; /// @@ -9,20 +11,20 @@ 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 */); - - /// Returns a forward offset of the current offset based on the cause. - Offset getForwardOffset(/* Cause */); + Position start(); + Position end(); /// For [TextNode] only. /// @@ -30,12 +32,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/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/flowy_key_event_handlers/arrow_keys_handler.dart deleted file mode 100644 index 3049f54453..0000000000 --- a/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_key_event_handlers/arrow_keys_handler.dart +++ /dev/null @@ -1,37 +0,0 @@ -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'; - -FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { - if (event.logicalKey != LogicalKeyboardKey.arrowUp && - event.logicalKey != LogicalKeyboardKey.arrowDown && - event.logicalKey != LogicalKeyboardKey.arrowLeft && - event.logicalKey != LogicalKeyboardKey.arrowRight) { - 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/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 new file mode 100644 index 0000000000..95496db2ea --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/internal_key_event_handlers/arrow_keys_handler.dart @@ -0,0 +1,14 @@ +import 'package:flowy_editor/service/keyboard_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +FlowyKeyEventHandler arrowKeysHandler = (editorState, event) { + if (event.logicalKey != LogicalKeyboardKey.arrowUp && + event.logicalKey != LogicalKeyboardKey.arrowDown && + event.logicalKey != LogicalKeyboardKey.arrowLeft && + event.logicalKey != LogicalKeyboardKey.arrowRight) { + return KeyEventResult.ignored; + } + + return KeyEventResult.ignored; +}; 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 96% 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 index db12d2bbb2..47a83f314a 100644 --- 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 @@ -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/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 69% 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 index 4e52d1bbe9..3eef8c1d1b 100644 --- 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 @@ -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 2f4bba86ec..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 @@ -1,68 +1,94 @@ +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'; 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: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 { - /// [Pan] and [Tap] must be mutually exclusive. - /// Pan - Offset? panStartOffset; - Offset? panEndOffset; + /// Returns the currently selected [Node]s. + /// + /// The order of the return is determined according to the selected order. + List get currentSelectedNodes; - /// Tap - Offset? tapOffset; + /// ------------------ Selection ------------------------ - void updateSelection(Offset start, Offset end); + /// + void updateSelection(Selection selection); - void updateCursor(Offset start); + /// + void clearSelection(); - /// Returns selected node(s) - /// Returns empty list if no nodes are being selected. - List getSelectedNodes(Offset start, [Offset? end]); + /// + List getNodesInSelection(Selection selection); - /// Compute selected node triggered by [Tap] - Node? computeSelectedNodeInOffset( - Node node, - Offset offset, - ); + /// ------------------ Selection ------------------------ - /// Compute selected nodes triggered by [Pan] - List computeSelectedNodesInRange( + /// ------------------ Offset ------------------------ + + /// 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 getNodesInRange(Offset start, [Offset? end]); + + /// Return the [Node] or [Null] in single selection. + /// + /// [start] is the offset under the global coordinate system. + 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 computeNodesInRange( Node node, Offset start, Offset end, ); - /// Pan - bool isNodeInSelection( + /// Return [bool] to identify the [Node] is in Range or not. + /// + /// [start] and [end] are the offsets under the global coordinate system. + bool isNodeInRange( 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); + + /// ------------------ 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,8 +101,23 @@ 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 + List currentSelectedNodes = []; + + @override + List getNodesInSelection(Selection selection) => + _selectedNodesInSelection(editorState.document.root, selection); + @override Widget build(BuildContext context) { return RawGestureDetector( @@ -105,76 +146,28 @@ class _FlowySelectionState extends State } @override - void updateSelection(Offset start, Offset end) { - _clearAllOverlayEntries(); + void updateSelection(Selection selection) { + _clearSelection(); - 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: Colors.yellow.withAlpha(100), - 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: Colors.red, - 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 + void clearSelection() { + _clearSelection(); + } + + @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]; } @@ -183,43 +176,49 @@ 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; } } - if (node.parent != null && node.key != null) { if (isNodeInOffset(node, offset)) { return node; } } - return null; } @override - List computeSelectedNodesInRange(Node node, Offset start, Offset end) { + 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 (isNodeInSelection(node, start, end)) { + if (isNodeInRange(node, start, end)) { result.add(node); } } for (final child in node.children) { - result.addAll(computeSelectedNodesInRange(child, start, end)); + result.addAll(computeNodesInRange(child, start, end)); } - // TODO: sort the result return result; } @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; @@ -229,10 +228,8 @@ 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?; + bool isNodeInRange(Node node, Offset start, Offset end) { + final renderBox = node.renderBox; if (renderBox != null) { final rect = Rect.fromPoints(start, end); final boxOffset = renderBox.localToGlobal(Offset.zero); @@ -243,59 +240,168 @@ 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; - updateCursor(tapOffset!); + tapOffset = details.globalPosition; + + 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) { - 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; - updateSelection(panStartOffset!, panEndOffset!); + 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) { + 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); + } } void _onPanEnd(DragEndDetails details) { // 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(); + // clear floating shortcusts + editorState.service.floatingShortcutServiceKey.currentState + ?.unwrapOrNull() + ?.hide(); } - void _clearFloatingShorts() { - final shortcutService = editorState - .service.floatingShortcutServiceKey.currentState - ?.unwrapOrNull(); - shortcutService?.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; + if (selectable == null) { + continue; + } + + 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 { + 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(), + ); + } + } + + 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 = 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) { + 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 (node.inSelection(selection)) { + result.add(node); + } + } + for (final child in node.children) { + result.addAll(_selectedNodesInSelection(child, selection)); + } + return result; } } 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 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', () {