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": {}
},
{
"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",
"delta": [

View File

@ -1,5 +1,4 @@
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/flowy_keyboard_service.dart';
import 'package:flutter/material.dart';
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;
@override
List<Rect> getSelectionRectsInSelection(Offset start, Offset end) {
List<Rect> getSelectionRectsInRange(Offset start, Offset end) {
final renderBox = context.findRenderObject() as RenderBox;
final size = renderBox.size;
final boxOffset = renderBox.localToGlobal(Offset.zero);
return [boxOffset & size];
return [Offset.zero & renderBox.size];
}
@override
Rect getCursorRect(Offset start) {
final renderBox = context.findRenderObject() as RenderBox;
final size = Size(5, renderBox.size.height);
final boxOffset = renderBox.localToGlobal(Offset.zero);
final cursorOffset =
Offset(renderBox.size.width + boxOffset.dx, boxOffset.dy);
final size = Size(2, renderBox.size.height);
final cursorOffset = Offset(renderBox.size.width, 0);
return cursorOffset & size;
}

View File

@ -54,7 +54,10 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
_textKey.currentContext?.findRenderObject() as RenderParagraph;
@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 =
TextSelection(baseOffset: 0, extentOffset: node.toRawString().length);
// 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);
_textSelection = textSelection;
if (end.dy > start.dy) {
if (localEnd.dy > localStart.dy) {
// downward
if (end.dy >= rects.last.bottom) {
if (localEnd.dy >= rects.last.bottom) {
return rects;
}
} else {
// upward
if (end.dy <= rects.first.top) {
if (localEnd.dy <= rects.first.top) {
return rects;
}
}
final selectionBaseOffset = _getTextPositionAtOffset(start).offset;
final selectionExtentOffset = _getTextPositionAtOffset(end).offset;
final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset;
final selectionExtentOffset = _getTextPositionAtOffset(localEnd).offset;
textSelection = TextSelection(
baseOffset: selectionBaseOffset,
extentOffset: selectionExtentOffset,
@ -86,7 +89,8 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
@override
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);
_textSelection = textSelection;
return _computeCursorRect(textSelection.baseOffset);
@ -99,7 +103,6 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
@override
Widget build(BuildContext context) {
print('text rebuild $this');
Widget richText;
if (kDebugMode) {
richText = DebuggableRichText(text: node.toTextSpan(), textKey: _textKey);
@ -132,23 +135,18 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
}
TextPosition _getTextPositionAtOffset(Offset offset) {
final textOffset = _renderParagraph.globalToLocal(offset);
return _renderParagraph.getPositionForOffset(textOffset);
return _renderParagraph.getPositionForOffset(offset);
}
List<Rect> _computeSelectionRects(TextSelection selection) {
final textBoxes = _renderParagraph.getBoxesForSelection(selection);
return textBoxes
.map((box) =>
_renderParagraph.localToGlobal(box.toRect().topLeft) &
box.toRect().size)
.toList();
return textBoxes.map((box) => box.toRect()).toList();
}
Rect _computeCursorRect(int offset) {
final position = TextPosition(offset: offset);
var cursorOffset = _renderParagraph.getOffsetForCaret(position, Rect.zero);
cursorOffset = _renderParagraph.localToGlobal(cursorOffset);
final cursorOffset =
_renderParagraph.getOffsetForCaret(position, Rect.zero);
final cursorHeight = _renderParagraph.getFullHeightForCaret(position);
if (cursorHeight != null) {
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 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/undo_manager.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 {
/// 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/render/render_plugins.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_builder.dart';
export 'package:flowy_editor/operation/operation.dart';
export 'package:flowy_editor/editor_state.dart';
export 'package:flowy_editor/flowy_editor_service.dart';
export 'package:flowy_editor/flowy_keyboard_service.dart';
export 'package:flowy_editor/service/flowy_editor_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,
child: CompositedTransformFollower(
link: widget.layerLink,
offset: Offset(widget.rect.center.dx, 0),
offset: widget.rect.topCenter,
showWhenUnlinked: true,
child: Container(
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> {
/// Returns a [Rect] list for overlay.
/// [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.
/// The return result must be an local offset.
Rect getCursorRect(Offset start);
/// For [TextNode] only.

View File

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

View File

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