feat: #953 improve arrow keys handler

This commit is contained in:
Lucas.Xu 2022-08-30 20:32:38 +08:00
parent b61e3f4601
commit f098c543e6
11 changed files with 710 additions and 132 deletions

View File

@ -46,9 +46,9 @@ class Selection {
(start.path <= end.path && !pathEquals(start.path, end.path)) ||
(isSingle && start.offset < end.offset);
Selection normalize() {
Selection get normalize {
if (isForward) {
return Selection(start: end, end: start);
return reversed;
}
return this;
}

View File

@ -32,7 +32,8 @@ mixin DefaultSelectable {
Selection getSelectionInRange(Offset start, Offset end) =>
forward.getSelectionInRange(start, end);
Offset localToGlobal(Offset offset) => forward.localToGlobal(offset);
Offset localToGlobal(Offset offset) =>
forward.localToGlobal(offset) - baseOffset;
Selection? getWorldBoundaryInOffset(Offset offset) =>
forward.getWorldBoundaryInOffset(offset);

View File

@ -132,17 +132,24 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
@override
List<Rect> getRectsInSelection(Selection selection) {
assert(pathEquals(selection.start.path, selection.end.path) &&
assert(selection.isSingle &&
pathEquals(selection.start.path, widget.textNode.path));
final textSelection = TextSelection(
baseOffset: selection.start.offset,
extentOffset: selection.end.offset,
);
return _renderParagraph
final rects = _renderParagraph
.getBoxesForSelection(textSelection, boxHeightStyle: BoxHeightStyle.max)
.map((box) => box.toRect())
.toList();
.toList(growable: false);
if (rects.isEmpty) {
// If the rich text widget does not contain any text,
// there will be no selection boxes,
// so we need to return to the default selection.
return [Rect.fromLTWH(0, 0, 0, _renderParagraph.size.height)];
}
return rects;
}
@override

View File

@ -1,26 +1,225 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/extensions/node_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
int _endOffsetOfNode(Node node) {
if (node is TextNode) {
return node.delta.length;
AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
if (!_arrowKeys.contains(event.logicalKey)) {
return KeyEventResult.ignored;
}
return 0;
if (event.isMetaPressed && event.isShiftPressed) {
return _arrowKeysWithMetaAndShift(editorState, event);
} else if (event.isMetaPressed) {
return _arrowKeysWithMeta(editorState, event);
} else if (event.isShiftPressed) {
return _arrowKeysWithShift(editorState, event);
} else {
return _arrowKeysOnly(editorState, event);
}
};
final _arrowKeys = [
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.arrowUp,
LogicalKeyboardKey.arrowDown
];
KeyEventResult _arrowKeysWithMetaAndShift(
EditorState editorState, RawKeyEvent event) {
if (!event.isMetaPressed ||
!event.isShiftPressed ||
!_arrowKeys.contains(event.logicalKey)) {
assert(false);
return KeyEventResult.ignored;
}
final nodes = editorState.service.selectionService.currentSelectedNodes;
final selection = editorState.service.selectionService.currentSelection.value;
if (nodes.isEmpty || selection == null) {
return KeyEventResult.ignored;
}
var start = selection.start;
var end = selection.end;
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
final position = nodes.first.selectable?.start();
if (position != null) {
end = position;
}
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
final position = nodes.first.selectable?.end();
if (position != null) {
end = position;
}
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
final position = editorState.document.root.children
.whereType<TextNode>()
.first
.selectable
?.start();
if (position != null) {
end = position;
}
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
final position = editorState.document.root.children
.whereType<TextNode>()
.last
.selectable
?.end();
if (position != null) {
end = position;
}
}
editorState.service.selectionService.updateSelection(
selection.copyWith(start: start, end: end),
);
return KeyEventResult.handled;
}
// Move the cursor to top, bottom, left and right of the document.
KeyEventResult _arrowKeysWithMeta(EditorState editorState, RawKeyEvent event) {
if (!event.isMetaPressed ||
event.isShiftPressed ||
!_arrowKeys.contains(event.logicalKey)) {
assert(false);
return KeyEventResult.ignored;
}
final nodes = editorState.service.selectionService.currentSelectedNodes;
if (nodes.isEmpty) {
return KeyEventResult.ignored;
}
Position? position;
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
position = nodes.first.selectable?.start();
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
position = nodes.last.selectable?.end();
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
position = editorState.document.root.children
.whereType<TextNode>()
.first
.selectable
?.start();
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
position = editorState.document.root.children
.whereType<TextNode>()
.last
.selectable
?.end();
}
if (position == null) {
return KeyEventResult.ignored;
}
editorState.service.selectionService.updateSelection(
Selection.collapsed(position),
);
return KeyEventResult.handled;
}
KeyEventResult _arrowKeysWithShift(EditorState editorState, RawKeyEvent event) {
if (event.isMetaPressed ||
!event.isShiftPressed ||
!_arrowKeys.contains(event.logicalKey)) {
assert(false);
return KeyEventResult.ignored;
}
final nodes = editorState.service.selectionService.currentSelectedNodes;
final selection = editorState.service.selectionService.currentSelection.value;
if (nodes.isEmpty || selection == null) {
return KeyEventResult.ignored;
}
Position? end;
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
end = selection.end.goLeft(editorState);
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
end = selection.end.goRight(editorState);
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
end = _goUp(editorState);
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
end = _goDown(editorState);
}
if (end == null) {
return KeyEventResult.ignored;
}
editorState.service.selectionService
.updateSelection(selection.copyWith(end: end));
return KeyEventResult.handled;
}
KeyEventResult _arrowKeysOnly(EditorState editorState, RawKeyEvent event) {
if (event.isMetaPressed ||
event.isShiftPressed ||
!_arrowKeys.contains(event.logicalKey)) {
assert(false);
return KeyEventResult.ignored;
}
final nodes = editorState.service.selectionService.currentSelectedNodes;
final selection =
editorState.service.selectionService.currentSelection.value?.normalize;
if (nodes.isEmpty || selection == null) {
return KeyEventResult.ignored;
}
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
if (selection.isCollapsed) {
final leftPosition = selection.start.goLeft(editorState);
if (leftPosition != null) {
editorState.service.selectionService.updateSelection(
Selection.collapsed(leftPosition),
);
}
} else {
editorState.service.selectionService.updateSelection(
Selection.collapsed(selection.start),
);
}
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
if (selection.isCollapsed) {
final rightPosition = selection.start.goRight(editorState);
if (rightPosition != null) {
editorState.service.selectionService.updateSelection(
Selection.collapsed(rightPosition),
);
}
} else {
editorState.service.selectionService.updateSelection(
Selection.collapsed(selection.end),
);
}
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
final upPosition = _goUp(editorState);
editorState.updateCursorSelection(
upPosition == null ? null : Selection.collapsed(upPosition),
);
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
final downPosition = _goDown(editorState);
editorState.updateCursorSelection(
downPosition == null ? null : Selection.collapsed(downPosition),
);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
extension on Position {
Position? goLeft(EditorState editorState) {
final node = editorState.document.nodeAtPath(path)!;
final node = editorState.document.nodeAtPath(path);
if (node == null) {
return null;
}
if (offset == 0) {
final prevNode = node.previous;
if (prevNode != null) {
return Position(
path: prevNode.path, offset: _endOffsetOfNode(prevNode));
final previousEnd = node.previous?.selectable?.end();
if (previousEnd != null) {
return previousEnd;
}
return null;
}
if (node is TextNode) {
return Position(path: path, offset: node.delta.prevRunePosition(offset));
} else {
@ -29,16 +228,18 @@ extension on Position {
}
Position? goRight(EditorState editorState) {
final node = editorState.document.nodeAtPath(path)!;
final lengthOfNode = _endOffsetOfNode(node);
if (offset >= lengthOfNode) {
final nextNode = node.next;
if (nextNode != null) {
return Position(path: nextNode.path, offset: 0);
final node = editorState.document.nodeAtPath(path);
if (node == null) {
return null;
}
final end = node.selectable?.end();
if (end != null && offset >= end.offset) {
final nextStart = node.next?.selectable?.start();
if (nextStart != null) {
return nextStart;
}
return null;
}
if (node is TextNode) {
return Position(path: path, offset: node.delta.nextRunePosition(offset));
} else {
@ -48,106 +249,43 @@ extension on Position {
}
Position? _goUp(EditorState editorState) {
final selection = editorState.service.selectionService.currentSelection.value;
final rects = editorState.service.selectionService.selectionRects;
if (rects.isEmpty) {
if (rects.isEmpty || selection == null) {
return null;
}
final first = rects.first;
final firstOffset = Offset(first.left, first.top);
final hitOffset = firstOffset - Offset(0, first.height * 0.5);
return editorState.service.selectionService.getPositionInOffset(hitOffset);
Offset offset;
if (selection.isBackward) {
final rect = rects.reduce(
(current, next) => current.bottom >= next.bottom ? current : next,
);
offset = rect.topRight.translate(0, -rect.height);
} else {
final rect = rects.reduce(
(current, next) => current.top <= next.top ? current : next,
);
offset = rect.topLeft.translate(0, -rect.height);
}
return editorState.service.selectionService.getPositionInOffset(offset);
}
Position? _goDown(EditorState editorState) {
final selection = editorState.service.selectionService.currentSelection.value;
final rects = editorState.service.selectionService.selectionRects;
if (rects.isEmpty) {
if (rects.isEmpty || selection == null) {
return null;
}
final first = rects.last;
final firstOffset = Offset(first.right, first.bottom);
final hitOffset = firstOffset + Offset(0, first.height * 0.5);
return editorState.service.selectionService.getPositionInOffset(hitOffset);
Offset offset;
if (selection.isBackward) {
final rect = rects.reduce(
(current, next) => current.bottom >= next.bottom ? current : next,
);
offset = rect.bottomRight.translate(0, rect.height);
} else {
final rect = rects.reduce(
(current, next) => current.top <= next.top ? current : next,
);
offset = rect.bottomLeft.translate(0, rect.height);
}
return editorState.service.selectionService.getPositionInOffset(offset);
}
KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) {
final currentSelection = editorState.cursorSelection;
if (currentSelection == null) {
return KeyEventResult.ignored;
}
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
final leftPosition = currentSelection.end.goLeft(editorState);
editorState.updateCursorSelection(leftPosition == null
? null
: Selection(start: currentSelection.start, end: leftPosition));
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
final rightPosition = currentSelection.start.goRight(editorState);
editorState.updateCursorSelection(rightPosition == null
? null
: Selection(start: rightPosition, end: currentSelection.end));
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
final position = _goUp(editorState);
editorState.updateCursorSelection(position == null
? null
: Selection(start: position, end: currentSelection.end));
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
final position = _goDown(editorState);
editorState.updateCursorSelection(position == null
? null
: Selection(start: currentSelection.start, end: position));
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
if (event.isShiftPressed) {
return _handleShiftKey(editorState, event);
}
final currentSelection = editorState.cursorSelection;
if (currentSelection == null) {
return KeyEventResult.ignored;
}
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
if (currentSelection.isCollapsed) {
final leftPosition = currentSelection.start.goLeft(editorState);
if (leftPosition != null) {
editorState.updateCursorSelection(Selection.collapsed(leftPosition));
}
} else {
editorState.updateCursorSelection(
currentSelection.collapse(atStart: currentSelection.isBackward),
);
}
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
if (currentSelection.isCollapsed) {
final rightPosition = currentSelection.end.goRight(editorState);
if (rightPosition != null) {
editorState.updateCursorSelection(Selection.collapsed(rightPosition));
}
} else {
editorState.updateCursorSelection(
currentSelection.collapse(atStart: !currentSelection.isBackward),
);
}
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
final position = _goUp(editorState);
editorState.updateCursorSelection(
position == null ? null : Selection.collapsed(position));
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
final position = _goDown(editorState);
editorState.updateCursorSelection(
position == null ? null : Selection.collapsed(position));
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
};

View File

@ -1,13 +1,12 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/infra/html_converter.dart';
import 'package:appflowy_editor/src/document/node_iterator.dart';
import 'package:appflowy_editor/src/infra/log.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:rich_clipboard/rich_clipboard.dart';
_handleCopy(EditorState editorState) async {
final selection = editorState.cursorSelection?.normalize();
final selection = editorState.cursorSelection?.normalize;
if (selection == null || selection.isCollapsed) {
return;
}
@ -43,7 +42,7 @@ _handleCopy(EditorState editorState) async {
}
_pasteHTML(EditorState editorState, String html) {
final selection = editorState.cursorSelection?.normalize();
final selection = editorState.cursorSelection?.normalize;
if (selection == null) {
return;
}
@ -191,7 +190,7 @@ Delta _lineContentToDelta(String lineContent) {
}
_handlePastePlainText(EditorState editorState, String plainText) {
final selection = editorState.cursorSelection?.normalize();
final selection = editorState.cursorSelection?.normalize;
if (selection == null) {
return;
}
@ -256,7 +255,7 @@ _handleCut(EditorState editorState) {
}
_deleteSelectedContent(EditorState editorState) {
final selection = editorState.cursorSelection?.normalize();
final selection = editorState.cursorSelection?.normalize;
if (selection == null || selection.isCollapsed) {
return;
}

View File

@ -1,7 +1,7 @@
import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart';
import 'package:appflowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
@ -13,6 +13,7 @@ import 'package:appflowy_editor/src/service/keyboard_service.dart';
List<AppFlowyKeyEventHandler> defaultKeyEventHandlers = [
deleteTextHandler,
slashShortcutHandler,
// arrowKeysHandler,
arrowKeysHandler,
copyPasteKeysHandler,
redoUndoKeysHandler,

View File

@ -0,0 +1,153 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
int _endOffsetOfNode(Node node) {
if (node is TextNode) {
return node.delta.length;
}
return 0;
}
extension on Position {
Position? goLeft(EditorState editorState) {
final node = editorState.document.nodeAtPath(path)!;
if (offset == 0) {
final prevNode = node.previous;
if (prevNode != null) {
return Position(
path: prevNode.path, offset: _endOffsetOfNode(prevNode));
}
return null;
}
if (node is TextNode) {
return Position(path: path, offset: node.delta.prevRunePosition(offset));
} else {
return Position(path: path, offset: offset);
}
}
Position? goRight(EditorState editorState) {
final node = editorState.document.nodeAtPath(path)!;
final lengthOfNode = _endOffsetOfNode(node);
if (offset >= lengthOfNode) {
final nextNode = node.next;
if (nextNode != null) {
return Position(path: nextNode.path, offset: 0);
}
return null;
}
if (node is TextNode) {
return Position(path: path, offset: node.delta.nextRunePosition(offset));
} else {
return Position(path: path, offset: offset);
}
}
}
Position? _goUp(EditorState editorState) {
final rects = editorState.service.selectionService.selectionRects;
if (rects.isEmpty) {
return null;
}
final first = rects.first;
final firstOffset = Offset(first.left, first.top);
final hitOffset = firstOffset - Offset(0, first.height * 0.5);
return editorState.service.selectionService.getPositionInOffset(hitOffset);
}
Position? _goDown(EditorState editorState) {
final rects = editorState.service.selectionService.selectionRects;
if (rects.isEmpty) {
return null;
}
final first = rects.last;
final firstOffset = Offset(first.right, first.bottom);
final hitOffset = firstOffset + Offset(0, first.height * 0.5);
return editorState.service.selectionService.getPositionInOffset(hitOffset);
}
KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) {
final currentSelection = editorState.cursorSelection;
if (currentSelection == null) {
return KeyEventResult.ignored;
}
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
final leftPosition = currentSelection.end.goLeft(editorState);
editorState.updateCursorSelection(leftPosition == null
? null
: Selection(start: currentSelection.start, end: leftPosition));
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
final rightPosition = currentSelection.start.goRight(editorState);
editorState.updateCursorSelection(rightPosition == null
? null
: Selection(start: rightPosition, end: currentSelection.end));
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
final position = _goUp(editorState);
editorState.updateCursorSelection(position == null
? null
: Selection(start: position, end: currentSelection.end));
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
final position = _goDown(editorState);
editorState.updateCursorSelection(position == null
? null
: Selection(start: currentSelection.start, end: position));
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
if (event.isShiftPressed) {
return _handleShiftKey(editorState, event);
}
final currentSelection = editorState.cursorSelection;
if (currentSelection == null) {
return KeyEventResult.ignored;
}
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
if (currentSelection.isCollapsed) {
final leftPosition = currentSelection.start.goLeft(editorState);
if (leftPosition != null) {
editorState.updateCursorSelection(Selection.collapsed(leftPosition));
}
} else {
editorState.updateCursorSelection(
currentSelection.collapse(atStart: currentSelection.isBackward),
);
}
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
if (currentSelection.isCollapsed) {
final rightPosition = currentSelection.end.goRight(editorState);
if (rightPosition != null) {
editorState.updateCursorSelection(Selection.collapsed(rightPosition));
}
} else {
editorState.updateCursorSelection(
currentSelection.collapse(atStart: !currentSelection.isBackward),
);
}
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
final position = _goUp(editorState);
editorState.updateCursorSelection(
position == null ? null : Selection.collapsed(position));
return KeyEventResult.handled;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
final position = _goDown(editorState);
editorState.updateCursorSelection(
position == null ? null : Selection.collapsed(position));
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
};

View File

@ -25,10 +25,6 @@ AppFlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
if (selection == null || context == null || selectable == null) {
return KeyEventResult.ignored;
}
final selectionRects = editorState.service.selectionService.selectionRects;
if (selectionRects.isEmpty) {
return KeyEventResult.ignored;
}
TransactionBuilder(editorState)
..replaceText(textNode, selection.start.offset,
selection.end.offset - selection.start.offset, event.character ?? '')

View File

@ -348,10 +348,8 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
final backwardNodes =
selection.isBackward ? nodes : nodes.reversed.toList(growable: false);
final backwardSelection = selection.isBackward
? selection
: selection.copyWith(start: selection.end, end: selection.start);
assert(backwardSelection.isBackward);
final normalizedSelection = selection.normalize;
assert(normalizedSelection.isBackward);
for (var i = 0; i < backwardNodes.length; i++) {
final node = backwardNodes[i];
@ -360,7 +358,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
continue;
}
var newSelection = backwardSelection.copy();
var newSelection = normalizedSelection.copy();
/// In the case of multiple selections,
/// we need to return a new selection for each selected node individually.
@ -370,7 +368,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
/// text: ghijkl
/// text: mn>opqr
///
if (!backwardSelection.isSingle) {
if (!normalizedSelection.isSingle) {
if (i == 0) {
newSelection = newSelection.copyWith(end: selectable.end());
} else if (i == nodes.length - 1) {

View File

@ -103,6 +103,9 @@ extension on LogicalKeyboardKey {
if (this == LogicalKeyboardKey.slash) {
return PhysicalKeyboardKey.slash;
}
if (this == LogicalKeyboardKey.arrowUp) {
return PhysicalKeyboardKey.arrowUp;
}
if (this == LogicalKeyboardKey.arrowDown) {
return PhysicalKeyboardKey.arrowDown;
}

View File

@ -58,6 +58,219 @@ void main() async {
(tester) async {
await _testPressArrowKeyInNotCollapsedSelection(tester, false);
});
testWidgets('Presses arrow left/right + shift in collapsed selection',
(tester) async {
const text = 'Welcome to Appflowy';
final editor = tester.editor
..insertTextNode(text)
..insertTextNode(text);
await editor.startTesting();
const offset = 8;
final selection = Selection.single(path: [1], startOffset: offset);
await editor.updateSelection(selection);
for (var i = offset - 1; i >= 0; i--) {
await editor.pressLogicKey(
LogicalKeyboardKey.arrowLeft,
isShiftPressed: true,
);
expect(
editor.documentSelection,
selection.copyWith(
end: Position(path: [1], offset: i),
),
);
}
for (var i = text.length; i >= 0; i--) {
await editor.pressLogicKey(
LogicalKeyboardKey.arrowLeft,
isShiftPressed: true,
);
expect(
editor.documentSelection,
selection.copyWith(
end: Position(path: [0], offset: i),
),
);
}
for (var i = 1; i <= text.length; i++) {
await editor.pressLogicKey(
LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
);
expect(
editor.documentSelection,
selection.copyWith(
end: Position(path: [0], offset: i),
),
);
}
for (var i = 0; i < text.length; i++) {
await editor.pressLogicKey(
LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
);
expect(
editor.documentSelection,
selection.copyWith(
end: Position(path: [1], offset: i),
),
);
}
});
testWidgets(
'Presses arrow left/right + shift in not collapsed and backward selection',
(tester) async {
const text = 'Welcome to Appflowy';
final editor = tester.editor
..insertTextNode(text)
..insertTextNode(text);
await editor.startTesting();
const start = 8;
const end = 12;
final selection = Selection.single(
path: [0],
startOffset: start,
endOffset: end,
);
await editor.updateSelection(selection);
for (var i = end + 1; i <= text.length; i++) {
await editor.pressLogicKey(
LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
);
expect(
editor.documentSelection,
selection.copyWith(
end: Position(path: [0], offset: i),
),
);
}
for (var i = text.length - 1; i >= 0; i--) {
await editor.pressLogicKey(
LogicalKeyboardKey.arrowLeft,
isShiftPressed: true,
);
expect(
editor.documentSelection,
selection.copyWith(
end: Position(path: [0], offset: i),
),
);
}
});
testWidgets(
'Presses arrow left/right + command in not collapsed and forward selection',
(tester) async {
const text = 'Welcome to Appflowy';
final editor = tester.editor
..insertTextNode(text)
..insertTextNode(text);
await editor.startTesting();
const start = 12;
const end = 8;
final selection = Selection.single(
path: [0],
startOffset: start,
endOffset: end,
);
await editor.updateSelection(selection);
for (var i = end - 1; i >= 0; i--) {
await editor.pressLogicKey(
LogicalKeyboardKey.arrowLeft,
isShiftPressed: true,
);
expect(
editor.documentSelection,
selection.copyWith(
end: Position(path: [0], offset: i),
),
);
}
for (var i = 1; i <= text.length; i++) {
await editor.pressLogicKey(
LogicalKeyboardKey.arrowRight,
isShiftPressed: true,
);
expect(
editor.documentSelection,
selection.copyWith(
end: Position(path: [0], offset: i),
),
);
}
});
testWidgets('Presses arrow left/right/up/down + meta in collapsed selection',
(tester) async {
await _testPressArrowKeyWithMetaInSelection(tester, true, false);
});
testWidgets(
'Presses arrow left/right/up/down + meta in not collapsed and backward selection',
(tester) async {
await _testPressArrowKeyWithMetaInSelection(tester, false, true);
});
testWidgets(
'Presses arrow left/right/up/down + meta in not collapsed and forward selection',
(tester) async {
await _testPressArrowKeyWithMetaInSelection(tester, false, false);
});
testWidgets('Presses arrow up/down + shift in not collapsed selection',
(tester) async {
const text = 'Welcome to Appflowy 😁';
final editor = tester.editor
..insertTextNode(text)
..insertTextNode(text)
..insertTextNode(null)
..insertTextNode(text)
..insertTextNode(null)
..insertTextNode(text)
..insertTextNode(text);
await editor.startTesting();
final selection = Selection.single(path: [3], startOffset: 8);
await editor.updateSelection(selection);
for (int i = 0; i < 3; i++) {
await editor.pressLogicKey(
LogicalKeyboardKey.arrowUp,
isShiftPressed: true,
);
}
expect(
editor.documentSelection,
selection.copyWith(
end: Position(path: [0], offset: 0),
),
);
for (int i = 0; i < 7; i++) {
await editor.pressLogicKey(
LogicalKeyboardKey.arrowDown,
isShiftPressed: true,
);
}
expect(
editor.documentSelection,
selection.copyWith(
end: Position(path: [6], offset: 0),
),
);
for (int i = 0; i < 3; i++) {
await editor.pressLogicKey(
LogicalKeyboardKey.arrowUp,
isShiftPressed: true,
);
}
expect(
editor.documentSelection,
selection.copyWith(
end: Position(path: [3], offset: 0),
),
);
});
}
Future<void> _testPressArrowKeyInNotCollapsedSelection(
@ -82,3 +295,72 @@ Future<void> _testPressArrowKeyInNotCollapsedSelection(
await editor.pressLogicKey(LogicalKeyboardKey.arrowRight);
expect(editor.documentSelection?.end, end);
}
Future<void> _testPressArrowKeyWithMetaInSelection(
WidgetTester tester,
bool isSingle,
bool isBackward,
) async {
const text = 'Welcome to Appflowy';
final editor = tester.editor
..insertTextNode(text)
..insertTextNode(text);
await editor.startTesting();
Selection selection;
if (isSingle) {
selection = Selection.single(
path: [0],
startOffset: 8,
);
} else {
if (isBackward) {
selection = Selection.single(
path: [0],
startOffset: 8,
endOffset: text.length,
);
} else {
selection = Selection.single(
path: [0],
startOffset: text.length,
endOffset: 8,
);
}
}
await editor.updateSelection(selection);
await editor.pressLogicKey(
LogicalKeyboardKey.arrowLeft,
isMetaPressed: true,
);
expect(
editor.documentSelection,
Selection.single(path: [0], startOffset: 0),
);
await editor.pressLogicKey(
LogicalKeyboardKey.arrowRight,
isMetaPressed: true,
);
expect(
editor.documentSelection,
Selection.single(path: [0], startOffset: text.length),
);
await editor.pressLogicKey(
LogicalKeyboardKey.arrowUp,
isMetaPressed: true,
);
expect(
editor.documentSelection,
Selection.single(path: [0], startOffset: 0),
);
await editor.pressLogicKey(
LogicalKeyboardKey.arrowDown,
isMetaPressed: true,
);
expect(
editor.documentSelection,
Selection.single(path: [1], startOffset: text.length),
);
}