Merge pull request #775 from LucasXu0/feat/selection_optimizatiion

feat: optimize selection implement by binary search
This commit is contained in:
Lucas.Xu 2022-08-08 11:07:05 +08:00 committed by GitHub
commit 9e254f84d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 42199 additions and 235 deletions

File diff suppressed because it is too large Load Diff

View File

@ -9,12 +9,6 @@
"image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png" "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png"
} }
}, },
{
"type": "youtube_link",
"attributes": {
"youtube_link": "https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
}
},
{ {
"type": "text", "type": "text",
"delta": [ "delta": [

View File

@ -56,7 +56,6 @@ class MyHomePage extends StatefulWidget {
} }
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
late EditorState _editorState;
final editorKey = GlobalKey(); final editorKey = GlobalKey();
int page = 0; int page = 0;
@ -69,137 +68,138 @@ class _MyHomePageState extends State<MyHomePage> {
title: Text(widget.title), title: Text(widget.title),
), ),
body: _buildBody(), body: _buildBody(),
floatingActionButton: ExpandableFab( floatingActionButton: _buildExpandableFab(),
distance: 112.0,
children: [
ActionButton(
onPressed: () {
if (page == 0) return;
setState(() {
page = 0;
});
},
icon: const Icon(Icons.note_add),
),
ActionButton(
icon: const Icon(Icons.document_scanner),
onPressed: () {
if (page == 1) return;
setState(() {
page = 1;
});
},
),
ActionButton(
onPressed: () {
if (page == 2) return;
setState(() {
page = 2;
});
},
icon: const Icon(Icons.text_fields),
),
],
),
); );
} }
Widget _buildBody() { Widget _buildBody() {
if (page == 0) { if (page == 0) {
return _buildFlowyEditor(); return _buildFlowyEditorWithExample();
} else if (page == 1) { } else if (page == 1) {
return _buildFlowyEditorWithEmptyDocument(); return _buildFlowyEditorWithEmptyDocument();
} else if (page == 2) { } else if (page == 2) {
return _buildTextField(); return _buildTextField();
} else if (page == 3) {
return _buildFlowyEditorWithBigDocument();
} }
return Container(); return Container();
} }
Widget _buildExpandableFab() {
return ExpandableFab(
distance: 112.0,
children: [
ActionButton(
onPressed: () {
if (page == 0) return;
setState(() {
page = 0;
});
},
icon: const Icon(Icons.note_add),
),
ActionButton(
icon: const Icon(Icons.document_scanner),
onPressed: () {
if (page == 1) return;
setState(() {
page = 1;
});
},
),
ActionButton(
onPressed: () {
if (page == 2) return;
setState(() {
page = 2;
});
},
icon: const Icon(Icons.text_fields),
),
ActionButton(
onPressed: () {
if (page == 3) return;
setState(() {
page = 3;
});
},
icon: const Icon(Icons.email),
),
],
);
}
Widget _buildFlowyEditorWithEmptyDocument() { Widget _buildFlowyEditorWithEmptyDocument() {
return Container( return _buildFlowyEditor(
padding: const EdgeInsets.only(left: 20, right: 20), EditorState(
child: FlowyEditor( document: StateTree(
key: editorKey, root: Node(
editorState: EditorState( type: 'editor',
document: StateTree( children: LinkedList()
root: Node( ..add(
type: 'editor', TextNode.empty()
children: LinkedList() ..delta = Delta(
..add( [TextInsert('')],
TextNode.empty() ),
..delta = Delta( ),
[TextInsert('')], attributes: {},
),
),
attributes: {},
),
), ),
), ),
keyEventHandlers: const [],
customBuilders: {
'image': ImageNodeBuilder(),
},
), ),
); );
} }
Widget _buildFlowyEditor() { Widget _buildFlowyEditorWithExample() {
return FutureBuilder<String>( return FutureBuilder<String>(
future: rootBundle.loadString('assets/example.json'), future: rootBundle.loadString('assets/example.json'),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (snapshot.hasData) {
final data = Map<String, Object>.from(json.decode(snapshot.data!));
return _buildFlowyEditor(EditorState(
document: StateTree.fromJson(data),
));
} else {
return const Center( return const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
} else { }
},
);
}
Widget _buildFlowyEditorWithBigDocument() {
return FutureBuilder<String>(
future: rootBundle.loadString('assets/big_document.json'),
builder: (context, snapshot) {
if (snapshot.hasData) {
final data = Map<String, Object>.from(json.decode(snapshot.data!)); final data = Map<String, Object>.from(json.decode(snapshot.data!));
final document = StateTree.fromJson(data); return _buildFlowyEditor(EditorState(
_editorState = EditorState( document: StateTree.fromJson(data),
document: document, ));
); } else {
return Container( return const Center(
padding: const EdgeInsets.only(left: 20, right: 20), child: CircularProgressIndicator(),
child: FlowyEditor(
key: editorKey,
editorState: _editorState,
keyEventHandlers: const [],
customBuilders: {
'image': ImageNodeBuilder(),
'youtube_link': YouTubeLinkNodeBuilder()
},
),
// shortcuts: [
// // TODO: this won't work, just a example for now.
// {
// 'h1': (editorState, eventName) {
// debugPrint('shortcut => $eventName');
// final selectedNodes = editorState.selectedNodes;
// if (selectedNodes.isEmpty) {
// return;
// }
// final textNode = selectedNodes.first as TextNode;
// TransactionBuilder(editorState)
// ..formatText(textNode, 0, textNode.toRawString().length, {
// 'heading': 'h1',
// })
// ..commit();
// }
// },
// {
// 'bold': (editorState, eventName) =>
// debugPrint('shortcut => $eventName')
// },
// {
// 'underline': (editorState, eventName) =>
// debugPrint('shortcut => $eventName')
// },
// ],
); );
} }
}, },
); );
} }
Widget _buildFlowyEditor(EditorState editorState) {
return Container(
padding: const EdgeInsets.only(left: 20, right: 20),
child: FlowyEditor(
key: editorKey,
editorState: editorState,
keyEventHandlers: const [],
customBuilders: {
'image': ImageNodeBuilder(),
'youtube_link': YouTubeLinkNodeBuilder()
},
),
);
}
Widget _buildTextField() { Widget _buildTextField() {
return const Center( return const Center(
child: TextField(), child: TextField(),

View File

@ -68,6 +68,7 @@ flutter:
assets: assets:
- document.json - document.json
- example.json - example.json
- big_document.json
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see

View File

@ -19,4 +19,12 @@ extension NodeExtensions on Node {
return selection.end.path <= path && path <= selection.start.path; return selection.end.path <= path && path <= selection.start.path;
} }
} }
Rect get rect {
if (renderBox != null) {
final boxOffset = renderBox!.localToGlobal(Offset.zero);
return boxOffset & renderBox!.size;
}
return Rect.zero;
}
} }

View File

@ -67,13 +67,13 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
void initState() { void initState() {
super.initState(); super.initState();
widget.editorState.service.selectionService.currentSelectedNodes widget.editorState.service.selectionService.currentSelection
.addListener(_onSelectionChange); .addListener(_onSelectionChange);
} }
@override @override
void dispose() { void dispose() {
widget.editorState.service.selectionService.currentSelectedNodes widget.editorState.service.selectionService.currentSelection
.removeListener(_onSelectionChange); .removeListener(_onSelectionChange);
super.dispose(); super.dispose();
} }

View File

@ -38,7 +38,7 @@ void formatBulletedList(EditorState editorState) {
} }
bool formatTextNodes(EditorState editorState, Attributes attributes) { bool formatTextNodes(EditorState editorState, Attributes attributes) {
final nodes = editorState.service.selectionService.currentSelectedNodes.value; final nodes = editorState.service.selectionService.currentSelectedNodes;
final textNodes = nodes.whereType<TextNode>().toList(); final textNodes = nodes.whereType<TextNode>().toList();
if (textNodes.isEmpty) { if (textNodes.isEmpty) {
@ -85,8 +85,8 @@ bool formatStrikethrough(EditorState editorState) {
} }
bool formatRichTextPartialStyle(EditorState editorState, String styleKey) { bool formatRichTextPartialStyle(EditorState editorState, String styleKey) {
final selection = editorState.service.selectionService.currentSelection; final selection = editorState.service.selectionService.currentSelection.value;
final nodes = editorState.service.selectionService.currentSelectedNodes.value; final nodes = editorState.service.selectionService.currentSelectedNodes;
final textNodes = nodes.whereType<TextNode>().toList(growable: false); final textNodes = nodes.whereType<TextNode>().toList(growable: false);
if (selection == null || textNodes.isEmpty) { if (selection == null || textNodes.isEmpty) {
@ -107,8 +107,8 @@ bool formatRichTextPartialStyle(EditorState editorState, String styleKey) {
} }
bool formatRichTextStyle(EditorState editorState, Attributes attributes) { bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
final selection = editorState.service.selectionService.currentSelection; final selection = editorState.service.selectionService.currentSelection.value;
final nodes = editorState.service.selectionService.currentSelectedNodes.value; final nodes = editorState.service.selectionService.currentSelectedNodes;
final textNodes = nodes.whereType<TextNode>().toList(); final textNodes = nodes.whereType<TextNode>().toList();
if (selection == null || textNodes.isEmpty) { if (selection == null || textNodes.isEmpty) {

View File

@ -42,15 +42,15 @@ class _FlowyInputState extends State<FlowyInput>
void initState() { void initState() {
super.initState(); super.initState();
_editorState.service.selectionService.currentSelectedNodes _editorState.service.selectionService.currentSelection
.addListener(_onSelectedNodesChange); .addListener(_onSelectionChange);
} }
@override @override
void dispose() { void dispose() {
close(); close();
_editorState.service.selectionService.currentSelectedNodes _editorState.service.selectionService.currentSelection
.removeListener(_onSelectedNodesChange); .removeListener(_onSelectionChange);
super.dispose(); super.dispose();
} }
@ -105,13 +105,12 @@ class _FlowyInputState extends State<FlowyInput>
void _applyInsert(TextEditingDeltaInsertion delta) { void _applyInsert(TextEditingDeltaInsertion delta) {
final selectionService = _editorState.service.selectionService; final selectionService = _editorState.service.selectionService;
final currentSelection = selectionService.currentSelection; final currentSelection = selectionService.currentSelection.value;
if (currentSelection == null) { if (currentSelection == null) {
return; return;
} }
if (currentSelection.isSingle) { if (currentSelection.isSingle) {
final textNode = final textNode = selectionService.currentSelectedNodes.first as TextNode;
selectionService.currentSelectedNodes.value.first as TextNode;
TransactionBuilder(_editorState) TransactionBuilder(_editorState)
..insertText( ..insertText(
textNode, textNode,
@ -126,13 +125,12 @@ class _FlowyInputState extends State<FlowyInput>
void _applyReplacement(TextEditingDeltaReplacement delta) { void _applyReplacement(TextEditingDeltaReplacement delta) {
final selectionService = _editorState.service.selectionService; final selectionService = _editorState.service.selectionService;
final currentSelection = selectionService.currentSelection; final currentSelection = selectionService.currentSelection.value;
if (currentSelection == null) { if (currentSelection == null) {
return; return;
} }
if (currentSelection.isSingle) { if (currentSelection.isSingle) {
final textNode = final textNode = selectionService.currentSelectedNodes.first as TextNode;
selectionService.currentSelectedNodes.value.first as TextNode;
final length = delta.replacedRange.end - delta.replacedRange.start; final length = delta.replacedRange.end - delta.replacedRange.start;
TransactionBuilder(_editorState) TransactionBuilder(_editorState)
..replaceText( ..replaceText(
@ -209,11 +207,11 @@ class _FlowyInputState extends State<FlowyInput>
// TODO: implement updateFloatingCursor // TODO: implement updateFloatingCursor
} }
void _onSelectedNodesChange() { void _onSelectionChange() {
final textNodes = _editorState final textNodes = _editorState.service.selectionService.currentSelectedNodes
.service.selectionService.currentSelectedNodes.value
.whereType<TextNode>(); .whereType<TextNode>();
final selection = _editorState.service.selectionService.currentSelection; final selection =
_editorState.service.selectionService.currentSelection.value;
// FIXME: upward and selection update. // FIXME: upward and selection update.
if (textNodes.isNotEmpty && selection != null) { if (textNodes.isNotEmpty && selection != null) {
final text = textNodes.fold<String>( final text = textNodes.fold<String>(

View File

@ -9,12 +9,12 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
final selection = editorState.service.selectionService.currentSelection; final selection = editorState.service.selectionService.currentSelection.value;
if (selection == null) { if (selection == null) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
final nodes = editorState.service.selectionService.currentSelectedNodes.value; final nodes = editorState.service.selectionService.currentSelectedNodes;
// make sure all nodes is [TextNode]. // make sure all nodes is [TextNode].
final textNodes = nodes.whereType<TextNode>().toList(); final textNodes = nodes.whereType<TextNode>().toList();
if (textNodes.length != nodes.length) { if (textNodes.length != nodes.length) {

View File

@ -18,8 +18,8 @@ FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
final nodes = editorState.service.selectionService.currentSelectedNodes.value; final nodes = editorState.service.selectionService.currentSelectedNodes;
final selection = editorState.service.selectionService.currentSelection; final selection = editorState.service.selectionService.currentSelection.value;
if (selection == null || if (selection == null ||
nodes.length != 1 || nodes.length != 1 ||
nodes.first is! TextNode || nodes.first is! TextNode ||

View File

@ -56,14 +56,13 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
final textNodes = editorState final textNodes = editorState.service.selectionService.currentSelectedNodes
.service.selectionService.currentSelectedNodes.value
.whereType<TextNode>(); .whereType<TextNode>();
if (textNodes.length != 1) { if (textNodes.length != 1) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
final selection = editorState.service.selectionService.currentSelection; final selection = editorState.service.selectionService.currentSelection.value;
final textNode = textNodes.first; final textNode = textNodes.first;
final context = textNode.context; final context = textNode.context;
final selectable = textNode.selectable; final selectable = textNode.selectable;
@ -97,9 +96,9 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
Overlay.of(context)?.insert(_popupListOverlay!); Overlay.of(context)?.insert(_popupListOverlay!);
editorState.service.selectionService.currentSelectedNodes editorState.service.selectionService.currentSelection
.removeListener(clearPopupListOverlay); .removeListener(clearPopupListOverlay);
editorState.service.selectionService.currentSelectedNodes editorState.service.selectionService.currentSelection
.addListener(clearPopupListOverlay); .addListener(clearPopupListOverlay);
// editorState.service.keyboardService?.disable(); // editorState.service.keyboardService?.disable();
_editorState = editorState; _editorState = editorState;

View File

@ -9,8 +9,8 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) {
return KeyEventResult.ignored; return KeyEventResult.ignored;
} }
final selection = editorState.service.selectionService.currentSelection; final selection = editorState.service.selectionService.currentSelection.value;
final nodes = editorState.service.selectionService.currentSelectedNodes.value; final nodes = editorState.service.selectionService.currentSelectedNodes;
final textNodes = nodes.whereType<TextNode>().toList(growable: false); final textNodes = nodes.whereType<TextNode>().toList(growable: false);
if (selection == null || textNodes.isEmpty) { if (selection == null || textNodes.isEmpty) {

View File

@ -17,20 +17,26 @@ import 'package:flowy_editor/render/selection/selection_widget.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> {
/// Returns the currently selected [Node]s. /// Returns the current [Selection]
ValueNotifier<Selection?> get currentSelection;
/// Returns the current selected [Node]s.
/// ///
/// The order of the return is determined according to the selected order. /// The order of the return is determined according to the selected order.
ValueNotifier<List<Node>> get currentSelectedNodes; List<Node> get currentSelectedNodes;
Selection? get currentSelection;
/// ------------------ Selection ------------------------
/// Update the selection or cursor.
/// ///
/// If selection is collapsed, this method will
/// update the position of the cursor.
/// Otherwise, will update the selection.
void updateSelection(Selection selection); void updateSelection(Selection selection);
/// /// Clear the selection or cursor.
void clearSelection(); void clearSelection();
/// ------------------ Selection ------------------------
List<Rect> rects(); List<Rect> rects();
Position? hitTest(Offset? offset); Position? hitTest(Offset? offset);
@ -42,34 +48,22 @@ mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
/// ------------------ Offset ------------------------ /// ------------------ Offset ------------------------
/// Returns selected [Node]s. Empty list would be returned
/// if no nodes are being selected.
///
///
/// [start] and [end] are the offsets under the global coordinate system.
///
/// If end is not null, it means multiple selection,
/// otherwise single selection.
List<Node> getNodesInRange(Offset start, [Offset? end]);
/// Return the [Node] or [Null] in single selection. /// Return the [Node] or [Null] in single selection.
/// ///
/// [start] is the offset under the global coordinate system. /// [offset] is under the global coordinate system.
Node? computeNodeInOffset(Node node, Offset offset); Node? getNodeInOffset(Offset offset);
/// Return the [Node]s in multiple selection. Empty list would be returned /// Returns selected [Node]s. Empty list would be returned
/// if no nodes are in range. /// if no nodes are in range.
/// ///
/// [start] is the offset under the global coordinate system. ///
List<Node> computeNodesInRange( /// [start] and [end] are under the global coordinate system.
Node node, ///
Offset start, List<Node> getNodeInRange(Offset start, Offset end);
Offset end,
);
/// 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 under the global coordinate system.
bool isNodeInRange( bool isNodeInRange(
Node node, Node node,
Offset start, Offset start,
@ -78,7 +72,7 @@ mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
/// Return [bool] to identify the [Node] contains [Offset] or not. /// Return [bool] to identify the [Node] contains [Offset] or not.
/// ///
/// [start] is the offset under the global coordinate system. /// [offset] is under the global coordinate system.
bool isNodeInOffset(Node node, Offset offset); bool isNodeInOffset(Node node, Offset offset);
/// ------------------ Offset ------------------------ /// ------------------ Offset ------------------------
@ -124,10 +118,10 @@ class _FlowySelectionState extends State<FlowySelection>
EditorState get editorState => widget.editorState; EditorState get editorState => widget.editorState;
@override @override
Selection? currentSelection; ValueNotifier<Selection?> currentSelection = ValueNotifier(null);
@override @override
ValueNotifier<List<Node>> currentSelectedNodes = ValueNotifier([]); List<Node> currentSelectedNodes = [];
@override @override
List<Node> getNodesInSelection(Selection selection) => List<Node> getNodesInSelection(Selection selection) =>
@ -145,8 +139,8 @@ class _FlowySelectionState extends State<FlowySelection>
super.didChangeMetrics(); super.didChangeMetrics();
// Need to refresh the selection when the metrics changed. // Need to refresh the selection when the metrics changed.
if (currentSelection != null) { if (currentSelection.value != null) {
updateSelection(currentSelection!); updateSelection(currentSelection.value!);
} }
} }
@ -192,8 +186,8 @@ class _FlowySelectionState extends State<FlowySelection>
@override @override
void clearSelection() { void clearSelection() {
currentSelection = null; currentSelectedNodes = [];
currentSelectedNodes.value = []; currentSelection.value = null;
// clear selection // clear selection
_selectionOverlays _selectionOverlays
@ -208,57 +202,15 @@ class _FlowySelectionState extends State<FlowySelection>
} }
@override @override
List<Node> getNodesInRange(Offset start, [Offset? end]) { Node? getNodeInOffset(Offset offset) {
if (end != null) { return _lowerBoundInDocument(offset);
return computeNodesInRange(editorState.document.root, start, end);
} else {
final result = computeNodeInOffset(editorState.document.root, start);
if (result != null) {
return [result];
}
}
return [];
} }
@override @override
Node? computeNodeInOffset(Node node, Offset offset) { List<Node> getNodeInRange(Offset start, Offset end) {
for (final child in node.children) { final startNode = _lowerBoundInDocument(start);
final result = computeNodeInOffset(child, offset); final endNode = _upperBoundInDocument(end);
if (result != null) { return NodeIterator(editorState.document, startNode, endNode).toList();
return result;
}
}
if (node.parent != null && node.key != null) {
if (isNodeInOffset(node, offset)) {
return node;
}
}
return null;
}
@override
List<Node> computeNodesInRange(Node node, Offset start, Offset end) {
final result = _computeNodesInRange(node, start, end);
if (start.dy <= end.dy) {
// downward
return result;
} else {
// upward
return result.reversed.toList(growable: false);
}
}
List<Node> _computeNodesInRange(Node node, Offset start, Offset end) {
List<Node> result = [];
if (node.parent != null && node.key != null) {
if (isNodeInRange(node, start, end)) {
result.add(node);
}
}
for (final child in node.children) {
result.addAll(_computeNodesInRange(child, start, end));
}
return result;
} }
@override @override
@ -286,12 +238,12 @@ class _FlowySelectionState extends State<FlowySelection>
void _onDoubleTapDown(TapDownDetails details) { void _onDoubleTapDown(TapDownDetails details) {
final offset = details.globalPosition; final offset = details.globalPosition;
final nodes = getNodesInRange(offset); final node = getNodeInOffset(offset);
if (nodes.isEmpty) { if (node == null) {
editorState.updateCursorSelection(null); editorState.updateCursorSelection(null);
return; return;
} }
final selectable = nodes.first.selectable; final selectable = node.selectable;
if (selectable == null) { if (selectable == null) {
editorState.updateCursorSelection(null); editorState.updateCursorSelection(null);
return; return;
@ -321,13 +273,12 @@ class _FlowySelectionState extends State<FlowySelection>
editorState.updateCursorSelection(null); editorState.updateCursorSelection(null);
return null; return null;
} }
final nodes = getNodesInRange(offset); final node = getNodeInOffset(offset);
if (nodes.isEmpty) { if (node == null) {
editorState.updateCursorSelection(null); editorState.updateCursorSelection(null);
return null; return null;
} }
assert(nodes.length == 1); final selectable = node.selectable;
final selectable = nodes.first.selectable;
if (selectable == null) { if (selectable == null) {
editorState.updateCursorSelection(null); editorState.updateCursorSelection(null);
return null; return null;
@ -359,12 +310,14 @@ class _FlowySelectionState extends State<FlowySelection>
panStartOffsetWithScrollDyGap = panStartOffsetWithScrollDyGap =
panStartOffsetWithScrollDyGap.translate(0, panStartScrollDy! - dy); panStartOffsetWithScrollDyGap.translate(0, panStartScrollDy! - dy);
} }
final nodes = getNodesInRange(panStartOffsetWithScrollDyGap, panEndOffset!);
if (nodes.isEmpty) { final sortedNodes =
return; editorState.document.root.children.toList(growable: false);
} final first = _lowerBound(
final first = nodes.first.selectable; sortedNodes, panStartOffsetWithScrollDyGap, 0, sortedNodes.length)
final last = nodes.last.selectable; .selectable;
final last = _upperBound(sortedNodes, panEndOffset!, 0, sortedNodes.length)
.selectable;
// compute the selection in range. // compute the selection in range.
if (first != null && last != null) { if (first != null && last != null) {
@ -397,8 +350,8 @@ class _FlowySelectionState extends State<FlowySelection>
void _updateSelection(Selection selection) { void _updateSelection(Selection selection) {
final nodes = _selectedNodesInSelection(editorState.document, selection); final nodes = _selectedNodesInSelection(editorState.document, selection);
currentSelection = selection; currentSelectedNodes = nodes;
currentSelectedNodes.value = nodes; currentSelection.value = selection;
Rect? topmostRect; Rect? topmostRect;
LayerLink? layerLink; LayerLink? layerLink;
@ -478,8 +431,8 @@ class _FlowySelectionState extends State<FlowySelection>
return; return;
} }
currentSelection = Selection.collapsed(position); currentSelectedNodes = [node];
currentSelectedNodes.value = [node]; currentSelection.value = Selection.collapsed(position);
final selectable = node.selectable; final selectable = node.selectable;
final rect = selectable?.getCursorRectInPosition(position); final rect = selectable?.getCursorRectInPosition(position);
@ -559,6 +512,57 @@ class _FlowySelectionState extends State<FlowySelection>
} }
} }
} }
Node _lowerBoundInDocument(Offset offset) {
final sortedNodes =
editorState.document.root.children.toList(growable: false);
return _lowerBound(sortedNodes, offset, 0, sortedNodes.length);
}
Node _upperBoundInDocument(Offset offset) {
final sortedNodes =
editorState.document.root.children.toList(growable: false);
return _upperBound(sortedNodes, offset, 0, sortedNodes.length);
}
/// TODO: Supports multi-level nesting,
/// currently only single-level nesting is supported
// find the first node's rect.bottom <= offset.dy
Node _lowerBound(List<Node> sortedNodes, Offset offset, int start, int end) {
var min = start;
var max = end;
while (min <= max) {
final mid = min + ((max - min) >> 1);
if (sortedNodes[mid].rect.bottom <= offset.dy) {
min = mid + 1;
} else {
max = mid - 1;
}
}
return sortedNodes[min];
}
/// TODO: Supports multi-level nesting,
/// currently only single-level nesting is supported
// find the first node's rect.top < offset.dy
Node _upperBound(
List<Node> sortedNodes,
Offset offset,
int start,
int end,
) {
var min = start;
var max = end;
while (min <= max) {
final mid = min + ((max - min) >> 1);
if (sortedNodes[mid].rect.top < offset.dy) {
min = mid + 1;
} else {
max = mid - 1;
}
}
return sortedNodes[max];
}
} }
/// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer] /// Because the flutter's [DoubleTapGestureRecognizer] will block the [TapGestureRecognizer]