mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: compute cursor and selection by [Selection] or [Offset]
This commit is contained in:
parent
114ae2b45d
commit
cde2127dec
@ -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 {
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
@ -31,4 +31,7 @@ class Position {
|
|||||||
offset: offset ?? this.offset,
|
offset: offset ?? this.offset,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'path = $path, offset = $offset';
|
||||||
}
|
}
|
||||||
|
@ -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';
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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', () {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user