Pārlūkot izejas kodu

feat: add a floating cursor and follow the document scroll. refactor the keyboard handler to a Function.

Lucas.Xu 2 gadi atpakaļ
vecāks
revīzija
a6ede7dc75

+ 6 - 0
frontend/app_flowy/packages/flowy_editor/example/assets/document.json

@@ -3,6 +3,12 @@
     "type": "editor",
     "type": "editor",
     "attributes": {},
     "attributes": {},
     "children": [
     "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",
         "type": "text",
         "delta": [
         "delta": [

+ 3 - 0
frontend/app_flowy/packages/flowy_editor/example/lib/main.dart

@@ -96,6 +96,9 @@ class _MyHomePageState extends State<MyHomePage> {
             );
             );
             return FlowyEditor(
             return FlowyEditor(
               editorState: _editorState,
               editorState: _editorState,
+              keyEventHandler: [
+                deleteSingleImageNode,
+              ],
             );
             );
           }
           }
         },
         },

+ 12 - 1
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_editor.dart';
+import 'package:flowy_editor/flowy_keyboard_service.dart';
 import 'package:flutter/material.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 {
 class ImageNodeBuilder extends NodeWidgetBuilder {
   ImageNodeBuilder.create({
   ImageNodeBuilder.create({

+ 3 - 0
frontend/app_flowy/packages/flowy_editor/lib/document/node.dart

@@ -11,6 +11,8 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
   final Attributes attributes;
   final Attributes attributes;
 
 
   GlobalKey? key;
   GlobalKey? key;
+  // TODO: abstract a selectable node??
+  final layerLink = LayerLink();
 
 
   String? get subtype {
   String? get subtype {
     // TODO: make 'subtype' as a const value.
     // TODO: make 'subtype' as a const value.
@@ -186,6 +188,7 @@ class TextNode extends Node {
     return map;
     return map;
   }
   }
 
 
+  // TODO: It's unneccesry to compute everytime.
   String toRawString() =>
   String toRawString() =>
       _delta.operations.whereType<TextInsert>().map((op) => op.content).join();
       _delta.operations.whereType<TextInsert>().map((op) => op.content).join();
 }
 }

+ 60 - 0
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<FlowyCursorWidget> createState() => _FlowyCursorWidgetState();
+}
+
+class _FlowyCursorWidgetState extends State<FlowyCursorWidget> {
+  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,
+        ),
+      ),
+    );
+  }
+}

+ 1 - 0
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/operation/operation.dart';
 export 'package:flowy_editor/editor_state.dart';
 export 'package:flowy_editor/editor_state.dart';
 export 'package:flowy_editor/flowy_editor_service.dart';
 export 'package:flowy_editor/flowy_editor_service.dart';
+export 'package:flowy_editor/flowy_keyboard_service.dart';

+ 4 - 3
frontend/app_flowy/packages/flowy_editor/lib/flowy_editor_service.dart

@@ -8,9 +8,11 @@ class FlowyEditor extends StatefulWidget {
   const FlowyEditor({
   const FlowyEditor({
     Key? key,
     Key? key,
     required this.editorState,
     required this.editorState,
+    required this.keyEventHandler,
   }) : super(key: key);
   }) : super(key: key);
 
 
   final EditorState editorState;
   final EditorState editorState;
+  final List<FlowyKeyEventHandler> keyEventHandler;
 
 
   @override
   @override
   State<FlowyEditor> createState() => _FlowyEditorState();
   State<FlowyEditor> createState() => _FlowyEditorState();
@@ -25,9 +27,8 @@ class _FlowyEditorState extends State<FlowyEditor> {
       editorState: editorState,
       editorState: editorState,
       child: FlowyKeyboardWidget(
       child: FlowyKeyboardWidget(
         handlers: [
         handlers: [
-          FlowyKeyboradBackSpaceHandler(
-            editorState: editorState,
-          )
+          flowyDeleteNodesHandler,
+          ...widget.keyEventHandler,
         ],
         ],
         editorState: editorState,
         editorState: editorState,
         child: editorState.build(context),
         child: editorState.build(context),

+ 22 - 44
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/operation/transaction_builder.dart';
-import 'package:flowy_editor/render/selectable.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 
 
 import 'editor_state.dart';
 import 'editor_state.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 
 
-abstract class FlowyKeyboardHandler {
-  final EditorState editorState;
-
-  FlowyKeyboardHandler({
-    required this.editorState,
-  });
-
-  KeyEventResult onKeyDown(RawKeyEvent event);
-}
-
-class FlowyKeyboradBackSpaceHandler extends FlowyKeyboardHandler {
-  FlowyKeyboradBackSpaceHandler({
-    required super.editorState,
-  });
+typedef FlowyKeyEventHandler = KeyEventResult Function(
+  EditorState editorState,
+  RawKeyEvent event,
+);
 
 
-  @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;
     return KeyEventResult.ignored;
   }
   }
-}
+
+  debugPrint('delete nodes = $nodes');
+
+  nodes
+      .fold<TransactionBuilder>(
+        TransactionBuilder(editorState),
+        (previousValue, node) => previousValue..deleteNode(node),
+      )
+      .commit();
+  return KeyEventResult.handled;
+};
 
 
 /// Process keyboard events
 /// Process keyboard events
 class FlowyKeyboardWidget extends StatefulWidget {
 class FlowyKeyboardWidget extends StatefulWidget {
@@ -60,7 +38,7 @@ class FlowyKeyboardWidget extends StatefulWidget {
 
 
   final EditorState editorState;
   final EditorState editorState;
   final Widget child;
   final Widget child;
-  final List<FlowyKeyboardHandler> handlers;
+  final List<FlowyKeyEventHandler> handlers;
 
 
   @override
   @override
   State<FlowyKeyboardWidget> createState() => _FlowyKeyboardWidgetState();
   State<FlowyKeyboardWidget> createState() => _FlowyKeyboardWidgetState();
@@ -89,7 +67,7 @@ class _FlowyKeyboardWidgetState extends State<FlowyKeyboardWidget> {
     for (final handler in widget.handlers) {
     for (final handler in widget.handlers) {
       debugPrint('handle keyboard event $event by $handler');
       debugPrint('handle keyboard event $event by $handler');
 
 
-      KeyEventResult result = handler.onKeyDown(event);
+      KeyEventResult result = handler(widget.editorState, event);
 
 
       switch (result) {
       switch (result) {
         case KeyEventResult.handled:
         case KeyEventResult.handled:
@@ -97,7 +75,7 @@ class _FlowyKeyboardWidgetState extends State<FlowyKeyboardWidget> {
         case KeyEventResult.skipRemainingHandlers:
         case KeyEventResult.skipRemainingHandlers:
           return KeyEventResult.skipRemainingHandlers;
           return KeyEventResult.skipRemainingHandlers;
         case KeyEventResult.ignored:
         case KeyEventResult.ignored:
-          break;
+          continue;
       }
       }
     }
     }
 
 

+ 17 - 21
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/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 
 
@@ -15,9 +16,9 @@ mixin _FlowySelectionService<T extends StatefulWidget> on State<T> {
   /// Tap
   /// Tap
   Offset? tapOffset;
   Offset? tapOffset;
 
 
-  void updateSelection();
+  void updateSelection(Offset start, Offset end);
 
 
-  void updateCursor();
+  void updateCursor(Offset offset);
 
 
   /// Returns selected node(s)
   /// Returns selected node(s)
   /// Returns empty list if no nodes are being selected.
   /// Returns empty list if no nodes are being selected.
@@ -66,6 +67,8 @@ class FlowySelectionWidget extends StatefulWidget {
 
 
 class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
 class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
     with _FlowySelectionService {
     with _FlowySelectionService {
+  final _cursorKey = GlobalKey(debugLabel: 'cursor');
+
   List<OverlayEntry> selectionOverlays = [];
   List<OverlayEntry> selectionOverlays = [];
 
 
   EditorState get editorState => widget.editorState;
   EditorState get editorState => widget.editorState;
@@ -98,14 +101,12 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
   }
   }
 
 
   @override
   @override
-  void updateSelection() {
+  void updateSelection(Offset start, Offset end) {
     _clearOverlay();
     _clearOverlay();
 
 
     final nodes = selectedNodes;
     final nodes = selectedNodes;
     editorState.selectedNodes = nodes;
     editorState.selectedNodes = nodes;
-    if (nodes.isEmpty || panStartOffset == null || panEndOffset == null) {
-      assert(panStartOffset == null);
-      assert(panEndOffset == null);
+    if (nodes.isEmpty) {
       return;
       return;
     }
     }
 
 
@@ -114,8 +115,8 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
         continue;
         continue;
       }
       }
       final selectable = node.key?.currentState as Selectable;
       final selectable = node.key?.currentState as Selectable;
-      final selectionRects = selectable.getSelectionRectsInSelection(
-          panStartOffset!, panEndOffset!);
+      final selectionRects =
+          selectable.getSelectionRectsInSelection(start, end);
       for (final rect in selectionRects) {
       for (final rect in selectionRects) {
         final overlay = OverlayEntry(
         final overlay = OverlayEntry(
           builder: ((context) => Positioned.fromRect(
           builder: ((context) => Positioned.fromRect(
@@ -132,14 +133,9 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
   }
   }
 
 
   @override
   @override
-  void updateCursor() {
+  void updateCursor(Offset offset) {
     _clearOverlay();
     _clearOverlay();
 
 
-    if (tapOffset == null) {
-      assert(tapOffset == null);
-      return;
-    }
-
     final nodes = selectedNodes;
     final nodes = selectedNodes;
     editorState.selectedNodes = nodes;
     editorState.selectedNodes = nodes;
     if (nodes.isEmpty) {
     if (nodes.isEmpty) {
@@ -151,13 +147,13 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
       return;
       return;
     }
     }
     final selectable = selectedNode.key?.currentState as Selectable;
     final selectable = selectedNode.key?.currentState as Selectable;
-    final rect = selectable.getCursorRect(tapOffset!);
+    final rect = selectable.getCursorRect(offset);
     final cursor = OverlayEntry(
     final cursor = OverlayEntry(
-      builder: ((context) => Positioned.fromRect(
+      builder: ((context) => FlowyCursorWidget(
+            key: _cursorKey,
             rect: rect,
             rect: rect,
-            child: Container(
-              color: Colors.blue,
-            ),
+            color: Colors.red,
+            layerLink: selectedNode.layerLink,
           )),
           )),
     );
     );
     selectionOverlays.add(cursor);
     selectionOverlays.add(cursor);
@@ -251,7 +247,7 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
     panStartOffset = null;
     panStartOffset = null;
     panEndOffset = null;
     panEndOffset = null;
 
 
-    updateCursor();
+    updateCursor(tapOffset!);
   }
   }
 
 
   void _onPanStart(DragStartDetails details) {
   void _onPanStart(DragStartDetails details) {
@@ -268,7 +264,7 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
     panEndOffset = details.globalPosition;
     panEndOffset = details.globalPosition;
     tapOffset = null;
     tapOffset = null;
 
 
-    updateSelection();
+    updateSelection(panStartOffset!, panEndOffset!);
   }
   }
 
 
   void _onPanEnd(DragEndDetails details) {
   void _onPanEnd(DragEndDetails details) {

+ 4 - 1
frontend/app_flowy/packages/flowy_editor/lib/render/node_widget_builder.dart

@@ -52,7 +52,10 @@ class NodeWidgetBuilder<T extends Node> {
       builder: (_, __) => Consumer<T>(
       builder: (_, __) => Consumer<T>(
         builder: ((context, value, child) {
         builder: ((context, value, child) {
           debugPrint('Node changed, and rebuilding...');
           debugPrint('Node changed, and rebuilding...');
-          return build(context);
+          return CompositedTransformTarget(
+            link: node.layerLink,
+            child: build(context),
+          );
         }),
         }),
       ),
       ),
     );
     );