feat: compute cursor and selection by [Selection] or [Offset]

This commit is contained in:
Lucas.Xu 2022-07-26 20:10:47 +08:00
parent 114ae2b45d
commit cde2127dec
8 changed files with 131 additions and 76 deletions

View File

@ -326,7 +326,7 @@ TextSelection? _globalSelectionToLocal(Node node, Selection? globalSel) {
if (!pathEquals(nodePath, globalSel.start.path)) { if (!pathEquals(nodePath, globalSel.start.path)) {
return null; return null;
} }
if (globalSel.isCollapsed()) { if (globalSel.isCollapsed) {
return TextSelection( return TextSelection(
baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset); baseOffset: globalSel.start.offset, extentOffset: globalSel.end.offset);
} else { } else {

View File

@ -7,27 +7,3 @@ typedef Path = List<int>;
bool pathEquals(Path path1, Path path2) { bool pathEquals(Path path1, Path path2) {
return listEquals(path1, path2); return listEquals(path1, path2);
} }
/// Returns true if path1 >= path2, otherwise returns false.
/// TODO: Rename this function.
bool pathGreaterOrEquals(Path path1, Path path2) {
final length = min(path1.length, path2.length);
for (var i = 0; i < length; i++) {
if (path1[i] < path2[i]) {
return false;
}
}
return true;
}
/// Returns true if path1 <= path2, otherwise returns false.
/// TODO: Rename this function.
bool pathLessOrEquals(Path path1, Path path2) {
final length = min(path1.length, path2.length);
for (var i = 0; i < length; i++) {
if (path1[i] > path2[i]) {
return false;
}
}
return true;
}

View File

@ -31,4 +31,7 @@ class Position {
offset: offset ?? this.offset, offset: offset ?? this.offset,
); );
} }
@override
String toString() => 'path = $path, offset = $offset';
} }

View File

@ -1,5 +1,6 @@
import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/path.dart';
import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/position.dart';
import 'package:flowy_editor/extensions/path_extensions.dart';
class Selection { class Selection {
final Position start; final Position start;
@ -29,9 +30,11 @@ class Selection {
} }
} }
bool isCollapsed() { bool get isCollapsed => start == end;
return start == end; bool get isUpward =>
} start.path >= end.path && !pathEquals(start.path, end.path);
bool get isDownward =>
start.path <= end.path && !pathEquals(start.path, end.path);
Selection copyWith({Position? start, Position? end}) { Selection copyWith({Position? start, Position? end}) {
return Selection( return Selection(
@ -39,4 +42,7 @@ class Selection {
end: end ?? this.end, end: end ?? this.end,
); );
} }
@override
String toString() => '[Selection] start = $start, end = $end';
} }

View File

@ -1,5 +1,9 @@
import 'dart:math';
import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/selection.dart';
import 'package:flowy_editor/extensions/object_extensions.dart'; import 'package:flowy_editor/extensions/object_extensions.dart';
import 'package:flowy_editor/extensions/path_extensions.dart';
import 'package:flowy_editor/render/selection/selectable.dart'; import 'package:flowy_editor/render/selection/selectable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -8,4 +12,12 @@ extension NodeExtensions on Node {
key?.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>(); key?.currentContext?.findRenderObject()?.unwrapOrNull<RenderBox>();
Selectable? get selectable => key?.currentState?.unwrapOrNull<Selectable>(); Selectable? get selectable => key?.currentState?.unwrapOrNull<Selectable>();
bool inSelection(Selection selection) {
if (selection.start.path <= selection.end.path) {
return selection.start.path <= path && path <= selection.end.path;
} else {
return selection.end.path <= path && path <= selection.start.path;
}
}
} }

View File

@ -0,0 +1,25 @@
import 'package:flowy_editor/document/path.dart';
import 'dart:math';
extension PathExtensions on Path {
bool operator >=(Path other) {
final length = min(this.length, other.length);
for (var i = 0; i < length; i++) {
if (this[i] < other[i]) {
return false;
}
}
return true;
}
bool operator <=(Path other) {
final length = min(this.length, other.length);
for (var i = 0; i < length; i++) {
if (this[i] > other[i]) {
return false;
}
}
return true;
}
}

View File

@ -1,4 +1,5 @@
import 'package:flowy_editor/document/path.dart'; import 'package:flowy_editor/document/path.dart';
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/document/position.dart'; import 'package:flowy_editor/document/position.dart';
import 'package:flowy_editor/document/selection.dart'; import 'package:flowy_editor/document/selection.dart';
import 'package:flowy_editor/render/selection/cursor_widget.dart'; import 'package:flowy_editor/render/selection/cursor_widget.dart';
@ -6,18 +7,22 @@ import 'package:flowy_editor/render/selection/flowy_selection_widget.dart';
import 'package:flowy_editor/extensions/object_extensions.dart'; import 'package:flowy_editor/extensions/object_extensions.dart';
import 'package:flowy_editor/extensions/node_extensions.dart'; import 'package:flowy_editor/extensions/node_extensions.dart';
import 'package:flowy_editor/service/shortcut_service.dart'; import 'package:flowy_editor/service/shortcut_service.dart';
import 'package:flowy_editor/editor_state.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 '../document/node.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> {
///
List<Node> get currentSelectedNodes;
/// ///
void updateSelection(Selection selection); void updateSelection(Selection selection);
///
void clearSelection();
/// Returns selected [Node]s. Empty list would be returned /// Returns selected [Node]s. Empty list would be returned
/// if no nodes are being selected. /// if no nodes are being selected.
/// ///
@ -49,7 +54,7 @@ mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
/// Return [bool] to identify the [Node] is in Range or not. /// Return [bool] to identify the [Node] is in Range or not.
/// ///
/// [start] and [end] are the offsets under the global coordinate system. /// [start] and [end] are the offsets under the global coordinate system.
bool isNodeInSelection( bool isNodeInRange(
Node node, Node node,
Offset start, Offset start,
Offset end, Offset end,
@ -96,6 +101,12 @@ class _FlowySelectionState extends State<FlowySelection>
EditorState get editorState => widget.editorState; EditorState get editorState => widget.editorState;
Node? _selectedNodeInPostion(Node node, Position position) =>
node.childAtPath(position.path);
@override
List<Node> currentSelectedNodes = [];
@override @override
List<Node> getNodesInSelection(Selection selection) => List<Node> getNodesInSelection(Selection selection) =>
_selectedNodesInSelection(editorState.document.root, selection); _selectedNodesInSelection(editorState.document.root, selection);
@ -129,16 +140,21 @@ class _FlowySelectionState extends State<FlowySelection>
@override @override
void updateSelection(Selection selection) { void updateSelection(Selection selection) {
_clearAllOverlayEntries(); _clearSelection();
// cursor // cursor
if (selection.isCollapsed()) { if (selection.isCollapsed) {
_updateCursor(selection.start); _updateCursor(selection.start);
} else { } else {
_updateSelection(selection); _updateSelection(selection);
} }
} }
@override
void clearSelection() {
_clearSelection();
}
@override @override
List<Node> getNodesInRange(Offset start, [Offset? end]) { List<Node> getNodesInRange(Offset start, [Offset? end]) {
if (end != null) { if (end != null) {
@ -172,7 +188,7 @@ class _FlowySelectionState extends State<FlowySelection>
List<Node> computeNodesInRange(Node node, Offset start, Offset end) { List<Node> computeNodesInRange(Node node, Offset start, Offset end) {
List<Node> result = []; List<Node> result = [];
if (node.parent != null && node.key != null) { if (node.parent != null && node.key != null) {
if (isNodeInSelection(node, start, end)) { if (isNodeInRange(node, start, end)) {
result.add(node); result.add(node);
} }
} }
@ -195,7 +211,7 @@ class _FlowySelectionState extends State<FlowySelection>
} }
@override @override
bool isNodeInSelection(Node node, Offset start, Offset end) { bool isNodeInRange(Node node, Offset start, Offset end) {
final renderBox = node.renderBox; final renderBox = node.renderBox;
if (renderBox != null) { if (renderBox != null) {
final rect = Rect.fromPoints(start, end); final rect = Rect.fromPoints(start, end);
@ -244,10 +260,21 @@ class _FlowySelectionState extends State<FlowySelection>
final first = nodes.first.selectable; final first = nodes.first.selectable;
final last = nodes.last.selectable; final last = nodes.last.selectable;
if (first != null && last != null) { if (first != null && last != null) {
final selection = Selection( final Selection selection;
start: first.getSelectionInRange(panStartOffset!, panEndOffset!).start, if (panStartOffset!.dy <= panEndOffset!.dy) {
// down
selection = Selection(
start:
first.getSelectionInRange(panStartOffset!, panEndOffset!).start,
end: last.getSelectionInRange(panStartOffset!, panEndOffset!).end, end: last.getSelectionInRange(panStartOffset!, panEndOffset!).end,
); );
} else {
// up
selection = Selection(
start: last.getSelectionInRange(panStartOffset!, panEndOffset!).end,
end: first.getSelectionInRange(panStartOffset!, panEndOffset!).start,
);
}
updateSelection(selection); updateSelection(selection);
} }
} }
@ -256,35 +283,29 @@ class _FlowySelectionState extends State<FlowySelection>
// do nothing // do nothing
} }
void _clearAllOverlayEntries() {
_clearSelection();
_clearCursor();
_clearFloatingShorts();
}
void _clearSelection() { void _clearSelection() {
currentSelectedNodes = [];
// clear selection
_selectionOverlays _selectionOverlays
..forEach((overlay) => overlay.remove()) ..forEach((overlay) => overlay.remove())
..clear(); ..clear();
} // clear cursors
void _clearCursor() {
_cursorOverlays _cursorOverlays
..forEach((overlay) => overlay.remove()) ..forEach((overlay) => overlay.remove())
..clear(); ..clear();
} // clear floating shortcusts
editorState.service.floatingShortcutServiceKey.currentState
void _clearFloatingShorts() { ?.unwrapOrNull<FlowyFloatingShortcutService>()
final shortcutService = editorState ?.hide();
.service.floatingShortcutServiceKey.currentState
?.unwrapOrNull<FlowyFloatingShortcutService>();
shortcutService?.hide();
} }
void _updateSelection(Selection selection) { void _updateSelection(Selection selection) {
final nodes = final nodes =
_selectedNodesInSelection(editorState.document.root, selection); _selectedNodesInSelection(editorState.document.root, selection);
currentSelectedNodes = nodes;
var index = 0; var index = 0;
for (final node in nodes) { for (final node in nodes) {
final selectable = node.selectable; final selectable = node.selectable;
@ -293,20 +314,38 @@ class _FlowySelectionState extends State<FlowySelection>
} }
Selection newSelection; Selection newSelection;
// TODO: too complicate, need to refactor.
if (node is TextNode) { if (node is TextNode) {
if (pathEquals(selection.start.path, selection.end.path)) { if (pathEquals(selection.start.path, selection.end.path)) {
newSelection = selection.copyWith(); newSelection = selection.copyWith();
} else { } else {
if (index == 0) { if (index == 0) {
if (selection.isUpward) {
newSelection = selection.copyWith( newSelection = selection.copyWith(
/// FIXME: make it better. /// FIXME: make it better.
end: selection.start.copyWith(offset: node.toRawString().length), start: selection.end.copyWith(),
end: selection.end.copyWith(offset: node.toRawString().length),
); );
} else {
newSelection = selection.copyWith(
/// FIXME: make it better.
end:
selection.start.copyWith(offset: node.toRawString().length),
);
}
} else if (index == nodes.length - 1) { } else if (index == nodes.length - 1) {
if (selection.isUpward) {
newSelection = selection.copyWith(
/// FIXME: make it better.
start: selection.start.copyWith(offset: 0),
end: selection.start.copyWith(),
);
} else {
newSelection = selection.copyWith( newSelection = selection.copyWith(
/// FIXME: make it better. /// FIXME: make it better.
start: selection.end.copyWith(offset: 0), start: selection.end.copyWith(offset: 0),
); );
}
} else { } else {
final position = Position(path: node.path); final position = Position(path: node.path);
newSelection = Selection( newSelection = Selection(
@ -339,13 +378,15 @@ class _FlowySelectionState extends State<FlowySelection>
} }
void _updateCursor(Position position) { void _updateCursor(Position position) {
final node = _selectedNodeInPostion(editorState.document.root, position); final node = editorState.document.root.childAtPath(position.path);
assert(node != null); assert(node != null);
if (node == null) { if (node == null) {
return; return;
} }
currentSelectedNodes = [node];
final selectable = node.selectable; final selectable = node.selectable;
final rect = selectable?.getCursorRectInPosition(position); final rect = selectable?.getCursorRectInPosition(position);
if (rect != null) { if (rect != null) {
@ -365,7 +406,7 @@ class _FlowySelectionState extends State<FlowySelection>
List<Node> _selectedNodesInSelection(Node node, Selection selection) { List<Node> _selectedNodesInSelection(Node node, Selection selection) {
List<Node> result = []; List<Node> result = [];
if (node.parent != null) { if (node.parent != null) {
if (_isNodeInSelection(node, selection)) { if (node.inSelection(selection)) {
result.add(node); result.add(node);
} }
} }
@ -374,12 +415,4 @@ class _FlowySelectionState extends State<FlowySelection>
} }
return result; return result;
} }
Node? _selectedNodeInPostion(Node node, Position position) =>
node.childAtPath(position.path);
bool _isNodeInSelection(Node node, Selection selection) {
return pathGreaterOrEquals(node.path, selection.start.path) &&
pathLessOrEquals(node.path, selection.end.path);
}
} }

View File

@ -127,7 +127,7 @@ void main() {
final pos = Position(path: [0], offset: 0); final pos = Position(path: [0], offset: 0);
final sel = Selection.collapsed(pos); final sel = Selection.collapsed(pos);
expect(sel.start, sel.end); expect(sel.start, sel.end);
expect(sel.isCollapsed(), true); expect(sel.isCollapsed, true);
}); });
test('test selection collapse', () { test('test selection collapse', () {