feat: transform betweens global/local cursor

This commit is contained in:
Vincent Chan 2022-07-19 16:56:53 +08:00
parent abe0658cd3
commit 8c6c9f7c0d
4 changed files with 122 additions and 30 deletions

View File

@ -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:flutter/gestures.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 {
final Node node;
final EditorState editorState;
@ -106,37 +142,80 @@ String _textContentOfDelta(Delta delta) {
class __TextNodeWidgetState extends State<_TextNodeWidget>
implements DeltaTextInputClient {
final _focusNode = FocusNode(debugLabel: "input");
TextNode get node => widget.node as TextNode;
EditorState get editorState => widget.editorState;
TextSelection? _localSelection;
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
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
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)
...node.children.map(
@ -165,7 +244,8 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
// TODO: implement currentTextEditingValue
TextEditingValue? get currentTextEditingValue => TextEditingValue(
text: _textContentOfDelta(node.delta),
selection: _localSelection ?? const TextSelection.collapsed(offset: -1));
selection: _globalSelectionToLocal(node, editorState.cursorSelection) ??
const TextSelection.collapsed(offset: 0));
@override
void insertTextPlaceholder(Size size) {
@ -174,7 +254,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
@override
void performAction(TextInputAction action) {
// TODO: implement performAction
debugPrint('action:$action');
}
@override
@ -204,6 +284,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
@override
void updateEditingValueWithDeltas(List<TextEditingDelta> textEditingDeltas) {
debugPrint(textEditingDeltas.toString());
for (final textDelta in textEditingDeltas) {
if (textDelta is TextEditingDeltaInsertion) {
TransactionBuilder(editorState)

View File

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

View File

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

View File

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