Browse Source

feat: transform betweens global/local cursor

Vincent Chan 2 years ago
parent
commit
8c6c9f7c0d

+ 103 - 22
frontend/app_flowy/packages/flowy_editor/example/lib/plugin/text_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/document/text_delta.dart';
 import 'package:flowy_editor/document/text_delta.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
@@ -81,6 +83,40 @@ extension on TextNode {
   }
   }
 }
 }
 
 
+TextSelection? _globalSelectionToLocal(Node node, Selection? globalSel) {
+  if (globalSel == null) {
+    return null;
+  }
+  final nodePath = node.path;
+
+  if (!pathEquals(nodePath, globalSel.start.path)) {
+    return null;
+  }
+  if (globalSel.isCollapsed()) {
+    return TextSelection(
+        baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset);
+  } else {
+    if (pathEquals(globalSel.start.path, globalSel.end.path)) {
+      return TextSelection(
+          baseOffset: globalSel.start.offset,
+          extentOffset: globalSel.end.offset);
+    }
+  }
+  return null;
+}
+
+Selection? _localSelectionToGlobal(Node node, TextSelection? sel) {
+  if (sel == null) {
+    return null;
+  }
+  final nodePath = node.path;
+
+  return Selection(
+    start: Position(path: nodePath, offset: sel.baseOffset),
+    end: Position(path: nodePath, offset: sel.extentOffset),
+  );
+}
+
 class _TextNodeWidget extends StatefulWidget {
 class _TextNodeWidget extends StatefulWidget {
   final Node node;
   final Node node;
   final EditorState editorState;
   final EditorState editorState;
@@ -106,37 +142,80 @@ String _textContentOfDelta(Delta delta) {
 
 
 class __TextNodeWidgetState extends State<_TextNodeWidget>
 class __TextNodeWidgetState extends State<_TextNodeWidget>
     implements DeltaTextInputClient {
     implements DeltaTextInputClient {
+  final _focusNode = FocusNode(debugLabel: "input");
   TextNode get node => widget.node as TextNode;
   TextNode get node => widget.node as TextNode;
   EditorState get editorState => widget.editorState;
   EditorState get editorState => widget.editorState;
-  TextSelection? _localSelection;
 
 
   TextInputConnection? _textInputConnection;
   TextInputConnection? _textInputConnection;
 
 
+  _backDeleteTextAtSelection(TextSelection? sel) {
+    if (sel == null) {
+      return;
+    }
+    if (sel.start == 0) {
+      return;
+    }
+
+    if (sel.isCollapsed) {
+      TransactionBuilder(editorState)
+        ..deleteText(node, sel.start - 1, 1)
+        ..commit();
+    } else {
+      TransactionBuilder(editorState)
+        ..deleteText(node, sel.start, sel.extentOffset - sel.baseOffset)
+        ..commit();
+    }
+
+    _textInputConnection?.setEditingState(TextEditingValue(
+        text: _textContentOfDelta(node.delta),
+        selection: _globalSelectionToLocal(node, editorState.cursorSelection) ??
+            const TextSelection.collapsed(offset: 0)));
+  }
+
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     return Column(
     return Column(
       crossAxisAlignment: CrossAxisAlignment.start,
       crossAxisAlignment: CrossAxisAlignment.start,
       children: [
       children: [
-        SelectableText.rich(
-          TextSpan(
-            children: node.toTextSpans(),
-          ),
-          onSelectionChanged: ((selection, cause) {
-            _textInputConnection?.close();
-            _textInputConnection = TextInput.attach(
-              this,
-              const TextInputConfiguration(
-                enableDeltaModel: true,
-                inputType: TextInputType.multiline,
-                textCapitalization: TextCapitalization.sentences,
-              ),
-            );
-            debugPrint('selection: $selection');
-            _textInputConnection
-              ?..show()
-              ..setEditingState(TextEditingValue(
-                  text: _textContentOfDelta(node.delta), selection: selection));
+        KeyboardListener(
+          focusNode: _focusNode,
+          onKeyEvent: ((value) {
+            if (value is KeyDownEvent || value is KeyRepeatEvent) {
+              final sel =
+                  _globalSelectionToLocal(node, editorState.cursorSelection);
+              if (value.logicalKey.keyLabel == "Backspace") {
+                _backDeleteTextAtSelection(sel);
+              }
+            }
           }),
           }),
+          child: SelectableText.rich(
+            showCursor: true,
+            TextSpan(
+              children: node.toTextSpans(),
+            ),
+            onTap: () {
+              _focusNode.requestFocus();
+            },
+            onSelectionChanged: ((selection, cause) {
+              _textInputConnection?.close();
+              _textInputConnection = TextInput.attach(
+                this,
+                const TextInputConfiguration(
+                  enableDeltaModel: true,
+                  inputType: TextInputType.multiline,
+                  textCapitalization: TextCapitalization.sentences,
+                ),
+              );
+              debugPrint('selection: $selection');
+              editorState.cursorSelection =
+                  _localSelectionToGlobal(node, selection);
+              _textInputConnection
+                ?..show()
+                ..setEditingState(TextEditingValue(
+                    text: _textContentOfDelta(node.delta),
+                    selection: selection));
+            }),
+          ),
         ),
         ),
         if (node.children.isNotEmpty)
         if (node.children.isNotEmpty)
           ...node.children.map(
           ...node.children.map(
@@ -165,7 +244,8 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
   // TODO: implement currentTextEditingValue
   // TODO: implement currentTextEditingValue
   TextEditingValue? get currentTextEditingValue => TextEditingValue(
   TextEditingValue? get currentTextEditingValue => TextEditingValue(
       text: _textContentOfDelta(node.delta),
       text: _textContentOfDelta(node.delta),
-      selection: _localSelection ?? const TextSelection.collapsed(offset: -1));
+      selection: _globalSelectionToLocal(node, editorState.cursorSelection) ??
+          const TextSelection.collapsed(offset: 0));
 
 
   @override
   @override
   void insertTextPlaceholder(Size size) {
   void insertTextPlaceholder(Size size) {
@@ -174,7 +254,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
 
 
   @override
   @override
   void performAction(TextInputAction action) {
   void performAction(TextInputAction action) {
-    // TODO: implement performAction
+    debugPrint('action:$action');
   }
   }
 
 
   @override
   @override
@@ -204,6 +284,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
 
 
   @override
   @override
   void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
   void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
+    debugPrint(textEditingDeltas.toString());
     for (final textDelta in textEditingDeltas) {
     for (final textDelta in textEditingDeltas) {
       if (textDelta is TextEditingDeltaInsertion) {
       if (textDelta is TextEditingDeltaInsertion) {
         TransactionBuilder(editorState)
         TransactionBuilder(editorState)

+ 1 - 0
frontend/app_flowy/packages/flowy_editor/lib/editor_state.dart

@@ -34,6 +34,7 @@ class EditorState {
     for (final op in transaction.operations) {
     for (final op in transaction.operations) {
       _applyOperation(op);
       _applyOperation(op);
     }
     }
+    cursorSelection = transaction.afterSelection;
   }
   }
 
 
   void _applyOperation(Operation op) {
   void _applyOperation(Operation op) {

+ 4 - 2
frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart

@@ -19,10 +19,12 @@ import './operation.dart';
 @immutable
 @immutable
 class Transaction {
 class Transaction {
   final UnmodifiableListView<Operation> operations;
   final UnmodifiableListView<Operation> operations;
-  final Selection? cursorSelection;
+  final Selection? beforeSelection;
+  final Selection? afterSelection;
 
 
   const Transaction({
   const Transaction({
     required this.operations,
     required this.operations,
-    this.cursorSelection,
+    this.beforeSelection,
+    this.afterSelection,
   });
   });
 }
 }

+ 14 - 6
frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart

@@ -1,7 +1,9 @@
 import 'dart:collection';
 import 'dart:collection';
+import 'dart:math';
 import 'package:flowy_editor/editor_state.dart';
 import 'package:flowy_editor/editor_state.dart';
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/path.dart';
 import 'package:flowy_editor/document/path.dart';
+import 'package:flowy_editor/document/position.dart';
 import 'package:flowy_editor/document/text_delta.dart';
 import 'package:flowy_editor/document/text_delta.dart';
 import 'package:flowy_editor/document/attributes.dart';
 import 'package:flowy_editor/document/attributes.dart';
 import 'package:flowy_editor/document/selection.dart';
 import 'package:flowy_editor/document/selection.dart';
@@ -22,7 +24,8 @@ import './transaction.dart';
 class TransactionBuilder {
 class TransactionBuilder {
   final List<Operation> operations = [];
   final List<Operation> operations = [];
   EditorState state;
   EditorState state;
-  Selection? cursorSelection;
+  Selection? beforeSelection;
+  Selection? afterSelection;
 
 
   TransactionBuilder(this.state);
   TransactionBuilder(this.state);
 
 
@@ -33,12 +36,12 @@ class TransactionBuilder {
   }
   }
 
 
   insertNode(Path path, Node node) {
   insertNode(Path path, Node node) {
-    cursorSelection = state.cursorSelection;
+    beforeSelection = state.cursorSelection;
     add(InsertOperation(path: path, value: node));
     add(InsertOperation(path: path, value: node));
   }
   }
 
 
   updateNode(Node node, Attributes attributes) {
   updateNode(Node node, Attributes attributes) {
-    cursorSelection = state.cursorSelection;
+    beforeSelection = state.cursorSelection;
     add(UpdateOperation(
     add(UpdateOperation(
       path: node.path,
       path: node.path,
       attributes: Attributes.from(node.attributes)..addAll(attributes),
       attributes: Attributes.from(node.attributes)..addAll(attributes),
@@ -47,12 +50,12 @@ class TransactionBuilder {
   }
   }
 
 
   deleteNode(Node node) {
   deleteNode(Node node) {
-    cursorSelection = state.cursorSelection;
+    beforeSelection = state.cursorSelection;
     add(DeleteOperation(path: node.path, removedValue: node));
     add(DeleteOperation(path: node.path, removedValue: node));
   }
   }
 
 
   textEdit(TextNode node, Delta Function() f) {
   textEdit(TextNode node, Delta Function() f) {
-    cursorSelection = state.cursorSelection;
+    beforeSelection = state.cursorSelection;
     final path = node.path;
     final path = node.path;
 
 
     final delta = f();
     final delta = f();
@@ -64,6 +67,8 @@ class TransactionBuilder {
 
 
   insertText(TextNode node, int index, String content) {
   insertText(TextNode node, int index, String content) {
     textEdit(node, () => Delta().retain(index).insert(content));
     textEdit(node, () => Delta().retain(index).insert(content));
+    afterSelection = Selection.collapsed(
+        Position(path: node.path, offset: index + content.length));
   }
   }
 
 
   formatText(TextNode node, int index, int length, Attributes attributes) {
   formatText(TextNode node, int index, int length, Attributes attributes) {
@@ -72,6 +77,8 @@ class TransactionBuilder {
 
 
   deleteText(TextNode node, int index, int length) {
   deleteText(TextNode node, int index, int length) {
     textEdit(node, () => Delta().retain(index).delete(length));
     textEdit(node, () => Delta().retain(index).delete(length));
+    afterSelection =
+        Selection.collapsed(Position(path: node.path, offset: index));
   }
   }
 
 
   add(Operation op) {
   add(Operation op) {
@@ -95,7 +102,8 @@ class TransactionBuilder {
   Transaction _finish() {
   Transaction _finish() {
     return Transaction(
     return Transaction(
       operations: UnmodifiableListView(operations),
       operations: UnmodifiableListView(operations),
-      cursorSelection: cursorSelection,
+      beforeSelection: beforeSelection,
+      afterSelection: afterSelection,
     );
     );
   }
   }
 }
 }