From a6ede7dc7532246c3c99066792df45ddad89e0a6 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 22 Jul 2022 15:45:57 +0800 Subject: [PATCH] feat: add a floating cursor and follow the document scroll. refactor the keyboard handler to a Function. --- .../flowy_editor/example/assets/document.json | 6 ++ .../flowy_editor/example/lib/main.dart | 3 + .../example/lib/plugin/image_node_widget.dart | 13 +++- .../flowy_editor/lib/document/node.dart | 3 + .../flowy_editor/lib/flowy_cursor_widget.dart | 60 +++++++++++++++++ .../flowy_editor/lib/flowy_editor.dart | 1 + .../lib/flowy_editor_service.dart | 7 +- .../lib/flowy_keyboard_service.dart | 66 +++++++------------ .../lib/flowy_selection_service.dart | 38 +++++------ .../lib/render/node_widget_builder.dart | 5 +- 10 files changed, 132 insertions(+), 70 deletions(-) create mode 100644 frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart 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 8356e31f7a..e89a258206 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/example/assets/document.json @@ -3,6 +3,12 @@ "type": "editor", "attributes": {}, "children": [ + { + "type": "image", + "attributes": { + "image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png?format=1500w" + } + }, { "type": "text", "delta": [ 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 bdd5658e6b..1e047a23b4 100644 --- a/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart +++ b/frontend/app_flowy/packages/flowy_editor/example/lib/main.dart @@ -96,6 +96,9 @@ class _MyHomePageState extends State { ); return FlowyEditor( editorState: _editorState, + keyEventHandler: [ + deleteSingleImageNode, + ], ); } }, 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 b235f8f481..934974ce8c 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,6 +1,17 @@ import 'package:flowy_editor/flowy_editor.dart'; +import 'package:flowy_editor/flowy_keyboard_service.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; + +FlowyKeyEventHandler deleteSingleImageNode = (editorState, event) { + final selectNodes = editorState.selectedNodes; + if (selectNodes.length != 1 || selectNodes.first.type != 'image') { + return KeyEventResult.ignored; + } + TransactionBuilder(editorState) + ..deleteNode(selectNodes.first) + ..commit(); + return KeyEventResult.handled; +}; class ImageNodeBuilder extends NodeWidgetBuilder { ImageNodeBuilder.create({ diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 8010a40646..8b80fd0b51 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -11,6 +11,8 @@ class Node extends ChangeNotifier with LinkedListEntry { final Attributes attributes; GlobalKey? key; + // TODO: abstract a selectable node?? + final layerLink = LayerLink(); String? get subtype { // TODO: make 'subtype' as a const value. @@ -186,6 +188,7 @@ class TextNode extends Node { return map; } + // TODO: It's unneccesry to compute everytime. String toRawString() => _delta.operations.whereType().map((op) => op.content).join(); } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart new file mode 100644 index 0000000000..e9d3d62f54 --- /dev/null +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_cursor_widget.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +class FlowyCursorWidget extends StatefulWidget { + const FlowyCursorWidget({ + Key? key, + required this.layerLink, + required this.rect, + required this.color, + this.blinkingInterval = 0.5, + }) : super(key: key); + + final double blinkingInterval; + final Color color; + final Rect rect; + final LayerLink layerLink; + + @override + State createState() => _FlowyCursorWidgetState(); +} + +class _FlowyCursorWidgetState extends State { + bool showCursor = true; + late Timer timer; + + @override + void initState() { + super.initState(); + + timer = Timer.periodic( + Duration(milliseconds: (widget.blinkingInterval * 1000).toInt()), + (timer) { + setState(() { + showCursor = !showCursor; + }); + }); + } + + @override + void dispose() { + timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Positioned.fromRect( + rect: widget.rect, + child: CompositedTransformFollower( + link: widget.layerLink, + offset: Offset(widget.rect.center.dx, 0), + showWhenUnlinked: true, + child: Container( + color: showCursor ? widget.color : Colors.transparent, + ), + ), + ); + } +} 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 96fad4340b..117c71c4ed 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor.dart @@ -12,3 +12,4 @@ 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'; diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart index 01b4fdf419..b10f1282cd 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart @@ -8,9 +8,11 @@ class FlowyEditor extends StatefulWidget { const FlowyEditor({ Key? key, required this.editorState, + required this.keyEventHandler, }) : super(key: key); final EditorState editorState; + final List keyEventHandler; @override State createState() => _FlowyEditorState(); @@ -25,9 +27,8 @@ class _FlowyEditorState extends State { editorState: editorState, child: FlowyKeyboardWidget( handlers: [ - FlowyKeyboradBackSpaceHandler( - editorState: editorState, - ) + flowyDeleteNodesHandler, + ...widget.keyEventHandler, ], editorState: editorState, child: editorState.build(context), diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart index c7752abae5..65ab52dac9 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_keyboard_service.dart @@ -1,53 +1,31 @@ -import 'package:flowy_editor/document/node.dart'; -import 'package:flowy_editor/operation/transaction.dart'; import 'package:flowy_editor/operation/transaction_builder.dart'; -import 'package:flowy_editor/render/selectable.dart'; import 'package:flutter/services.dart'; import 'editor_state.dart'; import 'package:flutter/material.dart'; -abstract class FlowyKeyboardHandler { - final EditorState editorState; +typedef FlowyKeyEventHandler = KeyEventResult Function( + EditorState editorState, + RawKeyEvent event, +); - FlowyKeyboardHandler({ - required this.editorState, - }); - - KeyEventResult onKeyDown(RawKeyEvent event); -} - -class FlowyKeyboradBackSpaceHandler extends FlowyKeyboardHandler { - FlowyKeyboradBackSpaceHandler({ - required super.editorState, - }); - - @override - KeyEventResult onKeyDown(RawKeyEvent event) { - final selectedNodes = editorState.selectedNodes; - if (selectedNodes.isNotEmpty) { - // handle delete text - // TODO: type: cursor or selection - if (selectedNodes.length == 1) { - final node = selectedNodes.first; - if (node is TextNode) { - final selectable = node.key?.currentState as Selectable?; - final textSelection = selectable?.getTextSelection(); - if (textSelection != null) { - if (textSelection.isCollapsed) { - TransactionBuilder(editorState) - ..deleteText(node, textSelection.start - 1, 1) - ..commit(); - // TODO: update selection?? - } - } - } - } - return KeyEventResult.handled; - } +FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) { + // Handle delete nodes. + final nodes = editorState.selectedNodes; + if (nodes.length <= 1) { return KeyEventResult.ignored; } -} + + debugPrint('delete nodes = $nodes'); + + nodes + .fold( + TransactionBuilder(editorState), + (previousValue, node) => previousValue..deleteNode(node), + ) + .commit(); + return KeyEventResult.handled; +}; /// Process keyboard events class FlowyKeyboardWidget extends StatefulWidget { @@ -60,7 +38,7 @@ class FlowyKeyboardWidget extends StatefulWidget { final EditorState editorState; final Widget child; - final List handlers; + final List handlers; @override State createState() => _FlowyKeyboardWidgetState(); @@ -89,7 +67,7 @@ class _FlowyKeyboardWidgetState extends State { for (final handler in widget.handlers) { debugPrint('handle keyboard event $event by $handler'); - KeyEventResult result = handler.onKeyDown(event); + KeyEventResult result = handler(widget.editorState, event); switch (result) { case KeyEventResult.handled: @@ -97,7 +75,7 @@ class _FlowyKeyboardWidgetState extends State { case KeyEventResult.skipRemainingHandlers: return KeyEventResult.skipRemainingHandlers; case KeyEventResult.ignored: - break; + continue; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart b/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart index b2a9f2b9c8..b460df9ec2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/flowy_selection_service.dart @@ -1,3 +1,4 @@ +import 'package:flowy_editor/flowy_cursor_widget.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -15,9 +16,9 @@ mixin _FlowySelectionService on State { /// Tap Offset? tapOffset; - void updateSelection(); + void updateSelection(Offset start, Offset end); - void updateCursor(); + void updateCursor(Offset offset); /// Returns selected node(s) /// Returns empty list if no nodes are being selected. @@ -66,6 +67,8 @@ class FlowySelectionWidget extends StatefulWidget { class _FlowySelectionWidgetState extends State with _FlowySelectionService { + final _cursorKey = GlobalKey(debugLabel: 'cursor'); + List selectionOverlays = []; EditorState get editorState => widget.editorState; @@ -98,14 +101,12 @@ class _FlowySelectionWidgetState extends State } @override - void updateSelection() { + void updateSelection(Offset start, Offset end) { _clearOverlay(); final nodes = selectedNodes; editorState.selectedNodes = nodes; - if (nodes.isEmpty || panStartOffset == null || panEndOffset == null) { - assert(panStartOffset == null); - assert(panEndOffset == null); + if (nodes.isEmpty) { return; } @@ -114,8 +115,8 @@ class _FlowySelectionWidgetState extends State continue; } final selectable = node.key?.currentState as Selectable; - final selectionRects = selectable.getSelectionRectsInSelection( - panStartOffset!, panEndOffset!); + final selectionRects = + selectable.getSelectionRectsInSelection(start, end); for (final rect in selectionRects) { final overlay = OverlayEntry( builder: ((context) => Positioned.fromRect( @@ -132,14 +133,9 @@ class _FlowySelectionWidgetState extends State } @override - void updateCursor() { + void updateCursor(Offset offset) { _clearOverlay(); - if (tapOffset == null) { - assert(tapOffset == null); - return; - } - final nodes = selectedNodes; editorState.selectedNodes = nodes; if (nodes.isEmpty) { @@ -151,13 +147,13 @@ class _FlowySelectionWidgetState extends State return; } final selectable = selectedNode.key?.currentState as Selectable; - final rect = selectable.getCursorRect(tapOffset!); + final rect = selectable.getCursorRect(offset); final cursor = OverlayEntry( - builder: ((context) => Positioned.fromRect( + builder: ((context) => FlowyCursorWidget( + key: _cursorKey, rect: rect, - child: Container( - color: Colors.blue, - ), + color: Colors.red, + layerLink: selectedNode.layerLink, )), ); selectionOverlays.add(cursor); @@ -251,7 +247,7 @@ class _FlowySelectionWidgetState extends State panStartOffset = null; panEndOffset = null; - updateCursor(); + updateCursor(tapOffset!); } void _onPanStart(DragStartDetails details) { @@ -268,7 +264,7 @@ class _FlowySelectionWidgetState extends State panEndOffset = details.globalPosition; tapOffset = null; - updateSelection(); + updateSelection(panStartOffset!, panEndOffset!); } void _onPanEnd(DragEndDetails details) { diff --git a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart index f349a0fe3d..a3d35f9dad 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart @@ -52,7 +52,10 @@ class NodeWidgetBuilder { builder: (_, __) => Consumer( builder: ((context, value, child) { debugPrint('Node changed, and rebuilding...'); - return build(context); + return CompositedTransformTarget( + link: node.layerLink, + child: build(context), + ); }), ), );