From f58a6c9523c917e240c3caa8b2594840deae863d Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 25 Jul 2022 11:07:20 +0800 Subject: [PATCH] feat: implement floating cursor and selection --- .../flowy_editor/example/assets/document.json | 27 +++++++++ .../example/lib/plugin/image_node_widget.dart | 13 ++--- .../lib/plugin/selected_text_node_widget.dart | 32 +++++------ .../flowy_editor/lib/editor_state.dart | 12 ++-- .../flowy_editor/lib/flowy_editor.dart | 6 +- .../selection}/flowy_cursor_widget.dart | 2 +- .../selection/flowy_selection_widget.dart | 34 +++++++++++ .../render/{ => selection}/selectable.dart | 4 +- .../{ => service}/flowy_editor_service.dart | 8 +-- .../{ => service}/flowy_keyboard_service.dart | 2 +- .../flowy_selection_service.dart | 57 +++++++++++-------- 11 files changed, 133 insertions(+), 64 deletions(-) rename frontend/app_flowy/packages/flowy_editor/lib/{ => render/selection}/flowy_cursor_widget.dart (96%) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart rename frontend/app_flowy/packages/flowy_editor/lib/render/{ => selection}/selectable.dart (66%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => service}/flowy_editor_service.dart (79%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => service}/flowy_keyboard_service.dart (98%) rename frontend/app_flowy/packages/flowy_editor/lib/{ => service}/flowy_selection_service.dart (82%) diff --git a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json index e89a258206..350764f769 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -97,6 +97,33 @@ ], "attributes": {} }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, + { + "type": "text", + "delta": [ + { + "insert": "Click the '?' at the bottom right for help and support." + } + ], + "attributes": {} + }, { "type": "text", "delta": [ 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 934974ce8c..4b63e77f51 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,5 +1,4 @@ import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/flowy_keyboard_service.dart'; import 'package:flutter/material.dart'; FlowyKeyEventHandler deleteSingleImageNode = (editorState, event) { @@ -50,20 +49,16 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable { String get src => widget.node.attributes['image_src'] as String; @override - List getSelectionRectsInSelection(Offset start, Offset end) { + List getSelectionRectsInRange(Offset start, Offset end) { final renderBox = context.findRenderObject() as RenderBox; - final size = renderBox.size; - final boxOffset = renderBox.localToGlobal(Offset.zero); - return [boxOffset & size]; + return [Offset.zero & renderBox.size]; } @override Rect getCursorRect(Offset start) { final renderBox = context.findRenderObject() as RenderBox; - final size = Size(5, renderBox.size.height); - final boxOffset = renderBox.localToGlobal(Offset.zero); - final cursorOffset = - Offset(renderBox.size.width + boxOffset.dx, boxOffset.dy); + final size = Size(2, renderBox.size.height); + final cursorOffset = Offset(renderBox.size.width, 0); return cursorOffset & size; } 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 7ce7162b07..3783eab4fa 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 @@ -54,7 +54,10 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> _textKey.currentContext?.findRenderObject() as RenderParagraph; @override - List getSelectionRectsInSelection(Offset start, Offset end) { + List getSelectionRectsInRange(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 @@ -62,20 +65,20 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> var rects = _computeSelectionRects(textSelection); _textSelection = textSelection; - if (end.dy > start.dy) { + if (localEnd.dy > localStart.dy) { // downward - if (end.dy >= rects.last.bottom) { + if (localEnd.dy >= rects.last.bottom) { return rects; } } else { // upward - if (end.dy <= rects.first.top) { + if (localEnd.dy <= rects.first.top) { return rects; } } - final selectionBaseOffset = _getTextPositionAtOffset(start).offset; - final selectionExtentOffset = _getTextPositionAtOffset(end).offset; + final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset; + final selectionExtentOffset = _getTextPositionAtOffset(localEnd).offset; textSelection = TextSelection( baseOffset: selectionBaseOffset, extentOffset: selectionExtentOffset, @@ -86,7 +89,8 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> @override Rect getCursorRect(Offset start) { - final selectionBaseOffset = _getTextPositionAtOffset(start).offset; + final localStart = _renderParagraph.globalToLocal(start); + final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset; final textSelection = TextSelection.collapsed(offset: selectionBaseOffset); _textSelection = textSelection; return _computeCursorRect(textSelection.baseOffset); @@ -99,7 +103,6 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> @override Widget build(BuildContext context) { - print('text rebuild $this'); Widget richText; if (kDebugMode) { richText = DebuggableRichText(text: node.toTextSpan(), textKey: _textKey); @@ -132,23 +135,18 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget> } TextPosition _getTextPositionAtOffset(Offset offset) { - final textOffset = _renderParagraph.globalToLocal(offset); - return _renderParagraph.getPositionForOffset(textOffset); + return _renderParagraph.getPositionForOffset(offset); } List _computeSelectionRects(TextSelection selection) { final textBoxes = _renderParagraph.getBoxesForSelection(selection); - return textBoxes - .map((box) => - _renderParagraph.localToGlobal(box.toRect().topLeft) & - box.toRect().size) - .toList(); + return textBoxes.map((box) => box.toRect()).toList(); } Rect _computeCursorRect(int offset) { final position = TextPosition(offset: offset); - var cursorOffset = _renderParagraph.getOffsetForCaret(position, Rect.zero); - cursorOffset = _renderParagraph.localToGlobal(cursorOffset); + final cursorOffset = + _renderParagraph.getOffsetForCaret(position, Rect.zero); final cursorHeight = _renderParagraph.getFullHeightForCaret(position); if (cursorHeight != null) { const cursorWidth = 2; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart index ced48242e2..f1fa65b33d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart @@ -1,11 +1,13 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/operation/operation.dart'; import 'dart:async'; -import 'package:flowy_editor/flowy_editor.dart'; -import 'package:flowy_editor/undo_manager.dart'; import 'package:flutter/material.dart'; -import './document/selection.dart'; +import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/selection.dart'; +import 'package:flowy_editor/document/state_tree.dart'; +import 'package:flowy_editor/operation/operation.dart'; +import 'package:flowy_editor/operation/transaction.dart'; +import 'package:flowy_editor/undo_manager.dart'; +import 'package:flowy_editor/render/render_plugins.dart'; class ApplyOptions { /// This flag indicates that diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart index 117c71c4ed..19c94ef327 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -6,10 +6,10 @@ export 'package:flowy_editor/document/path.dart'; export 'package:flowy_editor/document/text_delta.dart'; export 'package:flowy_editor/render/render_plugins.dart'; export 'package:flowy_editor/render/node_widget_builder.dart'; -export 'package:flowy_editor/render/selectable.dart'; +export 'package:flowy_editor/render/selection/selectable.dart'; export 'package:flowy_editor/operation/transaction.dart'; export 'package:flowy_editor/operation/transaction_builder.dart'; export 'package:flowy_editor/operation/operation.dart'; export 'package:flowy_editor/editor_state.dart'; -export 'package:flowy_editor/flowy_editor_service.dart'; -export 'package:flowy_editor/flowy_keyboard_service.dart'; +export 'package:flowy_editor/service/flowy_editor_service.dart'; +export 'package:flowy_editor/service/flowy_keyboard_service.dart'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_cursor_widget.dart similarity index 96% rename from frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart rename to frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_cursor_widget.dart index e9d3d62f54..9ab61e5c47 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_cursor_widget.dart @@ -49,7 +49,7 @@ class _FlowyCursorWidgetState extends State { rect: widget.rect, child: CompositedTransformFollower( link: widget.layerLink, - offset: Offset(widget.rect.center.dx, 0), + offset: widget.rect.topCenter, showWhenUnlinked: true, child: Container( color: showCursor ? widget.color : Colors.transparent, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart new file mode 100644 index 0000000000..f3def681e1 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/flowy_selection_widget.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class FlowySelectionWidget extends StatefulWidget { + const FlowySelectionWidget({ + Key? key, + required this.layerLink, + required this.rect, + required this.color, + }) : super(key: key); + + final Color color; + final Rect rect; + final LayerLink layerLink; + + @override + State createState() => _FlowySelectionWidgetState(); +} + +class _FlowySelectionWidgetState extends State { + @override + Widget build(BuildContext context) { + return Positioned.fromRect( + rect: widget.rect, + child: CompositedTransformFollower( + link: widget.layerLink, + offset: widget.rect.topLeft, + showWhenUnlinked: true, + child: Container( + color: widget.color, + ), + ), + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart similarity index 66% rename from frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart rename to frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart index 3631da106f..1ba8f32b53 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/selectable.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/selection/selectable.dart @@ -4,9 +4,11 @@ import 'package:flutter/material.dart'; mixin Selectable on State { /// Returns a [Rect] list for overlay. /// [start] and [end] are global offsets. - List getSelectionRectsInSelection(Offset start, Offset end); + /// The return result must be an local offset. + List getSelectionRectsInRange(Offset start, Offset end); /// Returns a [Rect] for cursor. + /// The return result must be an local offset. Rect getCursorRect(Offset start); /// For [TextNode] only. diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_editor_service.dart similarity index 79% rename from frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/flowy_editor_service.dart index b10f1282cd..0703e75022 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_editor_service.dart @@ -1,7 +1,7 @@ -import 'package:flowy_editor/flowy_keyboard_service.dart'; -import 'package:flowy_editor/flowy_selection_service.dart'; +import 'package:flowy_editor/service/flowy_keyboard_service.dart'; +import 'package:flowy_editor/service/flowy_selection_service.dart'; -import 'editor_state.dart'; +import '../editor_state.dart'; import 'package:flutter/material.dart'; class FlowyEditor extends StatefulWidget { @@ -23,7 +23,7 @@ class _FlowyEditorState extends State { @override Widget build(BuildContext context) { - return FlowySelectionWidget( + return FlowySelectionService( editorState: editorState, child: FlowyKeyboardWidget( handlers: [ diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_keyboard_service.dart similarity index 98% rename from frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/flowy_keyboard_service.dart index 65ab52dac9..68f295e0bd 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_keyboard_service.dart @@ -1,7 +1,7 @@ import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flutter/services.dart'; -import 'editor_state.dart'; +import '../editor_state.dart'; import 'package:flutter/material.dart'; typedef FlowyKeyEventHandler = KeyEventResult Function( diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_selection_service.dart similarity index 82% rename from frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart rename to frontend/app_flowy/packages/flowy_editor/lib/service/flowy_selection_service.dart index b460df9ec2..b75ea5703b 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/service/flowy_selection_service.dart @@ -1,10 +1,11 @@ -import 'package:flowy_editor/flowy_cursor_widget.dart'; +import 'package:flowy_editor/render/selection/flowy_cursor_widget.dart'; +import 'package:flowy_editor/render/selection/flowy_selection_widget.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'editor_state.dart'; -import 'document/node.dart'; -import '../render/selectable.dart'; +import '../editor_state.dart'; +import '../document/node.dart'; +import '../render/selection/selectable.dart'; /// Process selection and cursor mixin _FlowySelectionService on State { @@ -51,8 +52,8 @@ mixin _FlowySelectionService on State { ); } -class FlowySelectionWidget extends StatefulWidget { - const FlowySelectionWidget({ +class FlowySelectionService extends StatefulWidget { + const FlowySelectionService({ Key? key, required this.editorState, required this.child, @@ -62,14 +63,15 @@ class FlowySelectionWidget extends StatefulWidget { final Widget child; @override - State createState() => _FlowySelectionWidgetState(); + State createState() => _FlowySelectionServiceState(); } -class _FlowySelectionWidgetState extends State +class _FlowySelectionServiceState extends State with _FlowySelectionService { final _cursorKey = GlobalKey(debugLabel: 'cursor'); - List selectionOverlays = []; + final List _selectionOverlays = []; + final List _cursorOverlays = []; EditorState get editorState => widget.editorState; @@ -102,7 +104,7 @@ class _FlowySelectionWidgetState extends State @override void updateSelection(Offset start, Offset end) { - _clearOverlay(); + _clearAllOverlayEntries(); final nodes = selectedNodes; editorState.selectedNodes = nodes; @@ -115,26 +117,24 @@ class _FlowySelectionWidgetState extends State continue; } final selectable = node.key?.currentState as Selectable; - final selectionRects = - selectable.getSelectionRectsInSelection(start, end); + final selectionRects = selectable.getSelectionRectsInRange(start, end); for (final rect in selectionRects) { final overlay = OverlayEntry( - builder: ((context) => Positioned.fromRect( + builder: ((context) => FlowySelectionWidget( + color: Colors.yellow.withAlpha(100), + layerLink: node.layerLink, rect: rect, - child: Container( - color: Colors.yellow.withAlpha(100), - ), )), ); - selectionOverlays.add(overlay); + _selectionOverlays.add(overlay); } } - Overlay.of(context)?.insertAll(selectionOverlays); + Overlay.of(context)?.insertAll(_selectionOverlays); } @override void updateCursor(Offset offset) { - _clearOverlay(); + _clearAllOverlayEntries(); final nodes = selectedNodes; editorState.selectedNodes = nodes; @@ -156,8 +156,8 @@ class _FlowySelectionWidgetState extends State layerLink: selectedNode.layerLink, )), ); - selectionOverlays.add(cursor); - Overlay.of(context)?.insertAll(selectionOverlays); + _cursorOverlays.add(cursor); + Overlay.of(context)?.insertAll(_cursorOverlays); } @override @@ -271,8 +271,19 @@ class _FlowySelectionWidgetState extends State // do nothing } - void _clearOverlay() { - selectionOverlays + void _clearAllOverlayEntries() { + _clearSelection(); + _clearCursor(); + } + + void _clearSelection() { + _selectionOverlays + ..forEach((overlay) => overlay.remove()) + ..clear(); + } + + void _clearCursor() { + _cursorOverlays ..forEach((overlay) => overlay.remove()) ..clear(); }