feat: add keyboard example

This commit is contained in:
Lucas.Xu 2022-07-22 00:46:25 +08:00
parent a831ddc589
commit c643c02887
9 changed files with 79 additions and 172 deletions

View File

@ -1,5 +1,4 @@
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class EditorNodeWidgetBuilder extends NodeWidgetBuilder {

View File

@ -57,6 +57,11 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget>
return cursorOffset & size;
}
@override
TextSelection? getTextSelection() {
return null;
}
@override
KeyEventResult onKeyDown(RawKeyEvent event) {
if (event.logicalKey == LogicalKeyboardKey.backspace) {

View File

@ -94,6 +94,11 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
return _computeCursorRect(textSelection.baseOffset);
}
@override
TextSelection? getTextSelection() {
return _textSelection;
}
@override
KeyEventResult onKeyDown(RawKeyEvent event) {
if (event.logicalKey == LogicalKeyboardKey.backspace) {
@ -111,9 +116,9 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
TransactionBuilder(editorState)
..deleteText(node, textSelection.start - 1, 1)
..commit();
final rect = _computeCursorRect(textSelection.baseOffset - 1);
editorState.tapOffset = rect.center;
editorState.updateCursor();
// final rect = _computeCursorRect(textSelection.baseOffset - 1);
// editorState.tapOffset = rect.center;
// editorState.updateCursor();
}
} else {
TransactionBuilder(editorState)

View File

@ -15,10 +15,7 @@ import './render/render_plugins.dart';
class EditorState {
final StateTree document;
final RenderPlugins renderPlugins;
Offset? tapOffset;
Offset? panStartOffset;
Offset? panEndOffset;
List<Node> selectedNodes = [];
Selection? cursorSelection;
@ -59,148 +56,4 @@ class EditorState {
document.textEdit(op.path, op.delta);
}
}
List<OverlayEntry> selectionOverlays = [];
void updateCursor() {
selectionOverlays
..forEach((element) => element.remove())
..clear();
if (tapOffset == null) {
return;
}
// TODO: upward and backward
final selectedNode = _calculateSelectedNode(document.root, tapOffset!);
if (selectedNode.isEmpty) {
return;
}
final key = selectedNode.first.key;
if (key != null && key.currentState is Selectable) {
final selectable = key.currentState as Selectable;
final rect = selectable.getCursorRect(tapOffset!);
final overlay = OverlayEntry(builder: ((context) {
return Positioned.fromRect(
rect: rect,
child: Container(
color: Colors.red,
),
);
}));
selectionOverlays.add(overlay);
Overlay.of(selectable.context)?.insert(overlay);
}
}
void updateSelection() {
selectionOverlays
..forEach((element) => element.remove())
..clear();
final selectedNodes = this.selectedNodes;
if (selectedNodes.isEmpty ||
panStartOffset == null ||
panEndOffset == null) {
return;
}
for (final node in selectedNodes) {
final key = node.key;
if (key != null && key.currentState is Selectable) {
final selectable = key.currentState as Selectable;
final overlayRects = selectable.getSelectionRectsInSelection(
panStartOffset!, panEndOffset!);
for (final rect in overlayRects) {
// TODO: refactor overlay implement.
final overlay = OverlayEntry(builder: ((context) {
return Positioned.fromRect(
rect: rect,
child: Container(
color: Colors.yellow.withAlpha(100),
),
);
}));
selectionOverlays.add(overlay);
Overlay.of(selectable.context)?.insert(overlay);
}
}
}
}
List<Node> get selectedNodes {
if (panStartOffset != null && panEndOffset != null) {
return _calculateSelectedNodes(
document.root, panStartOffset!, panEndOffset!);
}
if (tapOffset != null) {
return _calculateSelectedNode(document.root, tapOffset!);
}
return [];
}
List<Node> _calculateSelectedNode(Node node, Offset offset) {
List<Node> result = [];
/// Skip the node without parent because it is the topmost node.
/// Skip the node without key because it cannot get the [RenderObject].
if (node.parent != null && node.key != null) {
if (_isNodeInOffset(node, offset)) {
result.add(node);
}
}
///
for (final child in node.children) {
result.addAll(_calculateSelectedNode(child, offset));
}
return result;
}
bool _isNodeInOffset(Node node, Offset offset) {
assert(node.key != null);
final renderBox =
node.key?.currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) {
return false;
}
final boxOffset = renderBox.localToGlobal(Offset.zero);
final boxRect = boxOffset & renderBox.size;
return boxRect.contains(offset);
}
List<Node> _calculateSelectedNodes(Node node, Offset start, Offset end) {
List<Node> result = [];
/// Skip the node without parent because it is the topmost node.
/// Skip the node without key because it cannot get the [RenderObject].
if (node.parent != null && node.key != null) {
if (_isNodeInRange(node, start, end)) {
result.add(node);
}
}
///
for (final child in node.children) {
result.addAll(_calculateSelectedNodes(child, start, end));
}
return result;
}
bool _isNodeInRange(Node node, Offset start, Offset end) {
assert(node.key != null);
final renderBox =
node.key?.currentContext?.findRenderObject() as RenderBox?;
/// Return false directly if the [RenderBox] cannot found.
if (renderBox == null) {
return false;
}
final rect = Rect.fromPoints(start, end);
final boxOffset = renderBox.localToGlobal(Offset.zero);
return rect.overlaps(boxOffset & renderBox.size);
}
}

View File

@ -24,7 +24,11 @@ class _FlowyEditorState extends State<FlowyEditor> {
return FlowySelectionWidget(
editorState: editorState,
child: FlowyKeyboardWidget(
handlers: const [],
handlers: [
FlowyKeyboradBackSpaceHandler(
editorState: editorState,
)
],
editorState: editorState,
child: editorState.build(context),
),

View File

@ -1,3 +1,7 @@
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';
@ -5,14 +9,44 @@ import 'package:flutter/material.dart';
abstract class FlowyKeyboardHandler {
final EditorState editorState;
final RawKeyEvent rawKeyEvent;
FlowyKeyboardHandler({
required this.editorState,
required this.rawKeyEvent,
});
KeyEventResult onKeyDown();
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;
}
return KeyEventResult.ignored;
}
}
/// Process keyboard events
@ -46,6 +80,8 @@ class _FlowyKeyboardWidgetState extends State<FlowyKeyboardWidget> {
}
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
debugPrint('on keyboard event $event');
if (event is! RawKeyDownEvent) {
return KeyEventResult.ignored;
}
@ -53,7 +89,7 @@ class _FlowyKeyboardWidgetState extends State<FlowyKeyboardWidget> {
for (final handler in widget.handlers) {
debugPrint('handle keyboard event $event by $handler');
KeyEventResult result = handler.onKeyDown();
KeyEventResult result = handler.onKeyDown(event);
switch (result) {
case KeyEventResult.handled:

View File

@ -102,6 +102,7 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
_clearOverlay();
final nodes = selectedNodes;
editorState.selectedNodes = nodes;
if (nodes.isEmpty || panStartOffset == null || panEndOffset == null) {
assert(panStartOffset == null);
assert(panEndOffset == null);
@ -139,6 +140,7 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
}
final nodes = selectedNodes;
editorState.selectedNodes = nodes;
if (nodes.isEmpty) {
return;
}

View File

@ -26,20 +26,20 @@ class Keyboard extends StatelessWidget {
}
KeyEventResult _onKey(FocusNode node, RawKeyEvent event) {
if (event is! RawKeyDownEvent) {
return KeyEventResult.ignored;
}
List<KeyEventResult> result = [];
for (final node in editorState.selectedNodes) {
if (node.key != null &&
node.key?.currentState is KeyboardEventsRespondable) {
final respondable = node.key!.currentState as KeyboardEventsRespondable;
result.add(respondable.onKeyDown(event));
}
}
if (result.contains(KeyEventResult.handled)) {
return KeyEventResult.handled;
}
// if (event is! RawKeyDownEvent) {
// return KeyEventResult.ignored;
// }
// List<KeyEventResult> result = [];
// for (final node in editorState.selectedNodes) {
// if (node.key != null &&
// node.key?.currentState is KeyboardEventsRespondable) {
// final respondable = node.key!.currentState as KeyboardEventsRespondable;
// result.add(respondable.onKeyDown(event));
// }
// }
// if (result.contains(KeyEventResult.handled)) {
// return KeyEventResult.handled;
// }
return KeyEventResult.ignored;
}
}

View File

@ -6,8 +6,11 @@ mixin Selectable<T extends StatefulWidget> on State<T> {
/// [start] and [end] are global offsets.
List<Rect> getSelectionRectsInSelection(Offset start, Offset end);
/// Returns a [Rect] for cursor
/// Returns a [Rect] for cursor.
Rect getCursorRect(Offset start);
/// For [TextNode] only.
TextSelection? getTextSelection();
}
mixin KeyboardEventsRespondable<T extends StatefulWidget> on State<T> {