feat: implement floating cursor and selection

This commit is contained in:
Lucas.Xu 2022-07-25 11:07:20 +08:00
parent a6ede7dc75
commit f58a6c9523
11 changed files with 133 additions and 64 deletions

View File

@ -97,6 +97,33 @@
], ],
"attributes": {} "attributes": {}
}, },
{
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support."
}
],
"attributes": {}
},
{
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support."
}
],
"attributes": {}
},
{
"type": "text",
"delta": [
{
"insert": "Click the '?' at the bottom right for help and support."
}
],
"attributes": {}
},
{ {
"type": "text", "type": "text",
"delta": [ "delta": [

View File

@ -1,5 +1,4 @@
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';
FlowyKeyEventHandler deleteSingleImageNode = (editorState, event) { FlowyKeyEventHandler deleteSingleImageNode = (editorState, event) {
@ -50,20 +49,16 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
String get src => widget.node.attributes['image_src'] as String; String get src => widget.node.attributes['image_src'] as String;
@override @override
List<Rect> getSelectionRectsInSelection(Offset start, Offset end) { List<Rect> getSelectionRectsInRange(Offset start, Offset end) {
final renderBox = context.findRenderObject() as RenderBox; final renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size; return [Offset.zero & renderBox.size];
final boxOffset = renderBox.localToGlobal(Offset.zero);
return [boxOffset & size];
} }
@override @override
Rect getCursorRect(Offset start) { Rect getCursorRect(Offset start) {
final renderBox = context.findRenderObject() as RenderBox; final renderBox = context.findRenderObject() as RenderBox;
final size = Size(5, renderBox.size.height); final size = Size(2, renderBox.size.height);
final boxOffset = renderBox.localToGlobal(Offset.zero); final cursorOffset = Offset(renderBox.size.width, 0);
final cursorOffset =
Offset(renderBox.size.width + boxOffset.dx, boxOffset.dy);
return cursorOffset & size; return cursorOffset & size;
} }

View File

@ -54,7 +54,10 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
_textKey.currentContext?.findRenderObject() as RenderParagraph; _textKey.currentContext?.findRenderObject() as RenderParagraph;
@override @override
List<Rect> getSelectionRectsInSelection(Offset start, Offset end) { List<Rect> getSelectionRectsInRange(Offset start, Offset end) {
final localStart = _renderParagraph.globalToLocal(start);
final localEnd = _renderParagraph.globalToLocal(end);
var textSelection = var textSelection =
TextSelection(baseOffset: 0, extentOffset: node.toRawString().length); TextSelection(baseOffset: 0, extentOffset: node.toRawString().length);
// Returns select all if the start or end exceeds the size of the box // Returns select all if the start or end exceeds the size of the box
@ -62,20 +65,20 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
var rects = _computeSelectionRects(textSelection); var rects = _computeSelectionRects(textSelection);
_textSelection = textSelection; _textSelection = textSelection;
if (end.dy > start.dy) { if (localEnd.dy > localStart.dy) {
// downward // downward
if (end.dy >= rects.last.bottom) { if (localEnd.dy >= rects.last.bottom) {
return rects; return rects;
} }
} else { } else {
// upward // upward
if (end.dy <= rects.first.top) { if (localEnd.dy <= rects.first.top) {
return rects; return rects;
} }
} }
final selectionBaseOffset = _getTextPositionAtOffset(start).offset; final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset;
final selectionExtentOffset = _getTextPositionAtOffset(end).offset; final selectionExtentOffset = _getTextPositionAtOffset(localEnd).offset;
textSelection = TextSelection( textSelection = TextSelection(
baseOffset: selectionBaseOffset, baseOffset: selectionBaseOffset,
extentOffset: selectionExtentOffset, extentOffset: selectionExtentOffset,
@ -86,7 +89,8 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
@override @override
Rect getCursorRect(Offset start) { Rect getCursorRect(Offset start) {
final selectionBaseOffset = _getTextPositionAtOffset(start).offset; final localStart = _renderParagraph.globalToLocal(start);
final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset;
final textSelection = TextSelection.collapsed(offset: selectionBaseOffset); final textSelection = TextSelection.collapsed(offset: selectionBaseOffset);
_textSelection = textSelection; _textSelection = textSelection;
return _computeCursorRect(textSelection.baseOffset); return _computeCursorRect(textSelection.baseOffset);
@ -99,7 +103,6 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
print('text rebuild $this');
Widget richText; Widget richText;
if (kDebugMode) { if (kDebugMode) {
richText = DebuggableRichText(text: node.toTextSpan(), textKey: _textKey); richText = DebuggableRichText(text: node.toTextSpan(), textKey: _textKey);
@ -132,23 +135,18 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
} }
TextPosition _getTextPositionAtOffset(Offset offset) { TextPosition _getTextPositionAtOffset(Offset offset) {
final textOffset = _renderParagraph.globalToLocal(offset); return _renderParagraph.getPositionForOffset(offset);
return _renderParagraph.getPositionForOffset(textOffset);
} }
List<Rect> _computeSelectionRects(TextSelection selection) { List<Rect> _computeSelectionRects(TextSelection selection) {
final textBoxes = _renderParagraph.getBoxesForSelection(selection); final textBoxes = _renderParagraph.getBoxesForSelection(selection);
return textBoxes return textBoxes.map((box) => box.toRect()).toList();
.map((box) =>
_renderParagraph.localToGlobal(box.toRect().topLeft) &
box.toRect().size)
.toList();
} }
Rect _computeCursorRect(int offset) { Rect _computeCursorRect(int offset) {
final position = TextPosition(offset: offset); final position = TextPosition(offset: offset);
var cursorOffset = _renderParagraph.getOffsetForCaret(position, Rect.zero); final cursorOffset =
cursorOffset = _renderParagraph.localToGlobal(cursorOffset); _renderParagraph.getOffsetForCaret(position, Rect.zero);
final cursorHeight = _renderParagraph.getFullHeightForCaret(position); final cursorHeight = _renderParagraph.getFullHeightForCaret(position);
if (cursorHeight != null) { if (cursorHeight != null) {
const cursorWidth = 2; const cursorWidth = 2;

View File

@ -1,11 +1,13 @@
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/operation/operation.dart';
import 'dart:async'; import 'dart:async';
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/undo_manager.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import './document/selection.dart'; import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/selection.dart';
import 'package:flowy_editor/document/state_tree.dart';
import 'package:flowy_editor/operation/operation.dart';
import 'package:flowy_editor/operation/transaction.dart';
import 'package:flowy_editor/undo_manager.dart';
import 'package:flowy_editor/render/render_plugins.dart';
class ApplyOptions { class ApplyOptions {
/// This flag indicates that /// This flag indicates that

View File

@ -6,10 +6,10 @@ export 'package:flowy_editor/document/path.dart';
export 'package:flowy_editor/document/text_delta.dart'; export 'package:flowy_editor/document/text_delta.dart';
export 'package:flowy_editor/render/render_plugins.dart'; export 'package:flowy_editor/render/render_plugins.dart';
export 'package:flowy_editor/render/node_widget_builder.dart'; export 'package:flowy_editor/render/node_widget_builder.dart';
export 'package:flowy_editor/render/selectable.dart'; export 'package:flowy_editor/render/selection/selectable.dart';
export 'package:flowy_editor/operation/transaction.dart'; export 'package:flowy_editor/operation/transaction.dart';
export 'package:flowy_editor/operation/transaction_builder.dart'; 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/service/flowy_editor_service.dart';
export 'package:flowy_editor/flowy_keyboard_service.dart'; export 'package:flowy_editor/service/flowy_keyboard_service.dart';

View File

@ -49,7 +49,7 @@ class _FlowyCursorWidgetState extends State<FlowyCursorWidget> {
rect: widget.rect, rect: widget.rect,
child: CompositedTransformFollower( child: CompositedTransformFollower(
link: widget.layerLink, link: widget.layerLink,
offset: Offset(widget.rect.center.dx, 0), offset: widget.rect.topCenter,
showWhenUnlinked: true, showWhenUnlinked: true,
child: Container( child: Container(
color: showCursor ? widget.color : Colors.transparent, color: showCursor ? widget.color : Colors.transparent,

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
class FlowySelectionWidget extends StatefulWidget {
const FlowySelectionWidget({
Key? key,
required this.layerLink,
required this.rect,
required this.color,
}) : super(key: key);
final Color color;
final Rect rect;
final LayerLink layerLink;
@override
State<FlowySelectionWidget> createState() => _FlowySelectionWidgetState();
}
class _FlowySelectionWidgetState extends State<FlowySelectionWidget> {
@override
Widget build(BuildContext context) {
return Positioned.fromRect(
rect: widget.rect,
child: CompositedTransformFollower(
link: widget.layerLink,
offset: widget.rect.topLeft,
showWhenUnlinked: true,
child: Container(
color: widget.color,
),
),
);
}
}

View File

@ -4,9 +4,11 @@ import 'package:flutter/material.dart';
mixin Selectable<T extends StatefulWidget> on State<T> { mixin Selectable<T extends StatefulWidget> on State<T> {
/// Returns a [Rect] list for overlay. /// Returns a [Rect] list for overlay.
/// [start] and [end] are global offsets. /// [start] and [end] are global offsets.
List<Rect> getSelectionRectsInSelection(Offset start, Offset end); /// The return result must be an local offset.
List<Rect> getSelectionRectsInRange(Offset start, Offset end);
/// Returns a [Rect] for cursor. /// Returns a [Rect] for cursor.
/// The return result must be an local offset.
Rect getCursorRect(Offset start); Rect getCursorRect(Offset start);
/// For [TextNode] only. /// For [TextNode] only.

View File

@ -1,7 +1,7 @@
import 'package:flowy_editor/flowy_keyboard_service.dart'; import 'package:flowy_editor/service/flowy_keyboard_service.dart';
import 'package:flowy_editor/flowy_selection_service.dart'; import 'package:flowy_editor/service/flowy_selection_service.dart';
import 'editor_state.dart'; import '../editor_state.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class FlowyEditor extends StatefulWidget { class FlowyEditor extends StatefulWidget {
@ -23,7 +23,7 @@ class _FlowyEditorState extends State<FlowyEditor> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FlowySelectionWidget( return FlowySelectionService(
editorState: editorState, editorState: editorState,
child: FlowyKeyboardWidget( child: FlowyKeyboardWidget(
handlers: [ handlers: [

View File

@ -1,7 +1,7 @@
import 'package:flowy_editor/operation/transaction_builder.dart'; import 'package:flowy_editor/operation/transaction_builder.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';
typedef FlowyKeyEventHandler = KeyEventResult Function( typedef FlowyKeyEventHandler = KeyEventResult Function(

View File

@ -1,10 +1,11 @@
import 'package:flowy_editor/flowy_cursor_widget.dart'; import 'package:flowy_editor/render/selection/flowy_cursor_widget.dart';
import 'package:flowy_editor/render/selection/flowy_selection_widget.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'editor_state.dart'; import '../editor_state.dart';
import 'document/node.dart'; import '../document/node.dart';
import '../render/selectable.dart'; import '../render/selection/selectable.dart';
/// Process selection and cursor /// Process selection and cursor
mixin _FlowySelectionService<T extends StatefulWidget> on State<T> { mixin _FlowySelectionService<T extends StatefulWidget> on State<T> {
@ -51,8 +52,8 @@ mixin _FlowySelectionService<T extends StatefulWidget> on State<T> {
); );
} }
class FlowySelectionWidget extends StatefulWidget { class FlowySelectionService extends StatefulWidget {
const FlowySelectionWidget({ const FlowySelectionService({
Key? key, Key? key,
required this.editorState, required this.editorState,
required this.child, required this.child,
@ -62,14 +63,15 @@ class FlowySelectionWidget extends StatefulWidget {
final Widget child; final Widget child;
@override @override
State<FlowySelectionWidget> createState() => _FlowySelectionWidgetState(); State<FlowySelectionService> createState() => _FlowySelectionServiceState();
} }
class _FlowySelectionWidgetState extends State<FlowySelectionWidget> class _FlowySelectionServiceState extends State<FlowySelectionService>
with _FlowySelectionService { with _FlowySelectionService {
final _cursorKey = GlobalKey(debugLabel: 'cursor'); final _cursorKey = GlobalKey(debugLabel: 'cursor');
List<OverlayEntry> selectionOverlays = []; final List<OverlayEntry> _selectionOverlays = [];
final List<OverlayEntry> _cursorOverlays = [];
EditorState get editorState => widget.editorState; EditorState get editorState => widget.editorState;
@ -102,7 +104,7 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
@override @override
void updateSelection(Offset start, Offset end) { void updateSelection(Offset start, Offset end) {
_clearOverlay(); _clearAllOverlayEntries();
final nodes = selectedNodes; final nodes = selectedNodes;
editorState.selectedNodes = nodes; editorState.selectedNodes = nodes;
@ -115,26 +117,24 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
continue; continue;
} }
final selectable = node.key?.currentState as Selectable; final selectable = node.key?.currentState as Selectable;
final selectionRects = final selectionRects = selectable.getSelectionRectsInRange(start, end);
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) => FlowySelectionWidget(
color: Colors.yellow.withAlpha(100),
layerLink: node.layerLink,
rect: rect, rect: rect,
child: Container(
color: Colors.yellow.withAlpha(100),
),
)), )),
); );
selectionOverlays.add(overlay); _selectionOverlays.add(overlay);
} }
} }
Overlay.of(context)?.insertAll(selectionOverlays); Overlay.of(context)?.insertAll(_selectionOverlays);
} }
@override @override
void updateCursor(Offset offset) { void updateCursor(Offset offset) {
_clearOverlay(); _clearAllOverlayEntries();
final nodes = selectedNodes; final nodes = selectedNodes;
editorState.selectedNodes = nodes; editorState.selectedNodes = nodes;
@ -156,8 +156,8 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
layerLink: selectedNode.layerLink, layerLink: selectedNode.layerLink,
)), )),
); );
selectionOverlays.add(cursor); _cursorOverlays.add(cursor);
Overlay.of(context)?.insertAll(selectionOverlays); Overlay.of(context)?.insertAll(_cursorOverlays);
} }
@override @override
@ -271,8 +271,19 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
// do nothing // do nothing
} }
void _clearOverlay() { void _clearAllOverlayEntries() {
selectionOverlays _clearSelection();
_clearCursor();
}
void _clearSelection() {
_selectionOverlays
..forEach((overlay) => overlay.remove())
..clear();
}
void _clearCursor() {
_cursorOverlays
..forEach((overlay) => overlay.remove()) ..forEach((overlay) => overlay.remove())
..clear(); ..clear();
} }