mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: implement floating cursor and selection
This commit is contained in:
parent
a6ede7dc75
commit
f58a6c9523
@ -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": [
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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.
|
@ -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: [
|
@ -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(
|
@ -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();
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user