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": {}
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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,
|
@ -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> {
|
||||
/// 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.
|
@ -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: [
|
@ -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(
|
@ -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(
|
||||
rect: rect,
|
||||
child: Container(
|
||||
builder: ((context) => FlowySelectionWidget(
|
||||
color: Colors.yellow.withAlpha(100),
|
||||
),
|
||||
layerLink: node.layerLink,
|
||||
rect: rect,
|
||||
)),
|
||||
);
|
||||
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();
|
||||
}
|
Loading…
Reference in New Issue
Block a user