Text editing shift + tap + drag interaction (#95213)

Supports the desktop text editing interaction of holding shift, tapping the field, and dragging to modify the selection.
This commit is contained in:
Justin McCandless 2022-02-04 13:20:03 -08:00 committed by GitHub
parent 45f8c39052
commit 734c3c4f8d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 872 additions and 21 deletions

View file

@ -1026,9 +1026,23 @@ class TextSelectionGestureDetectorBuilder {
// The viewport offset pixels of the [RenderEditable] at the last drag start.
double _dragStartViewportOffset = 0.0;
// Returns true iff either shift key is currently down.
bool get _isShiftPressed {
return HardwareKeyboard.instance.logicalKeysPressed
.any(<LogicalKeyboardKey>{
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
}.contains);
}
// True iff a tap + shift has been detected but the tap has not yet come up.
bool _isShiftTapping = false;
// For a shift + tap + drag gesture, the TextSelection at the point of the
// tap. Mac uses this value to reset to the original selection when an
// inversion of the base and offset happens.
TextSelection? _shiftTapDragSelection;
/// Handler for [TextSelectionGestureDetector.onTapDown].
///
/// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets
@ -1050,12 +1064,7 @@ class TextSelectionGestureDetectorBuilder {
|| kind == PointerDeviceKind.stylus;
// Handle shift + click selection if needed.
final bool isShiftPressed = HardwareKeyboard.instance.logicalKeysPressed
.any(<LogicalKeyboardKey>{
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
}.contains);
if (isShiftPressed && renderEditable.selection?.baseOffset != null) {
if (_isShiftPressed && renderEditable.selection?.baseOffset != null) {
_isShiftTapping = true;
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
@ -1290,10 +1299,27 @@ class TextSelectionGestureDetectorBuilder {
|| kind == PointerDeviceKind.touch
|| kind == PointerDeviceKind.stylus;
renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
if (_isShiftPressed && renderEditable.selection != null && renderEditable.selection!.isValid) {
_isShiftTapping = true;
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
_expandSelection(details.globalPosition, SelectionChangedCause.drag);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
_extendSelection(details.globalPosition, SelectionChangedCause.drag);
break;
}
_shiftTapDragSelection = renderEditable.selection;
} else {
renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
}
_dragStartViewportOffset = renderEditable.offset.pixels;
}
@ -1312,28 +1338,77 @@ class TextSelectionGestureDetectorBuilder {
if (!delegate.selectionEnabled)
return;
// Adjust the drag start offset for possible viewport offset changes.
final Offset startOffset = renderEditable.maxLines == 1
? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0)
: Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset);
if (!_isShiftTapping) {
// Adjust the drag start offset for possible viewport offset changes.
final Offset startOffset = renderEditable.maxLines == 1
? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0)
: Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset);
renderEditable.selectPositionAt(
from: startDetails.globalPosition - startOffset,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
return renderEditable.selectPositionAt(
from: startDetails.globalPosition - startOffset,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
}
if (_shiftTapDragSelection!.isCollapsed
|| (defaultTargetPlatform != TargetPlatform.iOS
&& defaultTargetPlatform != TargetPlatform.macOS)) {
return _extendSelection(updateDetails.globalPosition, SelectionChangedCause.drag);
}
// If the drag inverts the selection, Mac and iOS revert to the initial
// selection.
final TextSelection selection = editableText.textEditingValue.selection;
final TextPosition nextExtent = renderEditable.getPositionForPoint(updateDetails.globalPosition);
final bool isShiftTapDragSelectionForward =
_shiftTapDragSelection!.baseOffset < _shiftTapDragSelection!.extentOffset;
final bool isInverted = isShiftTapDragSelectionForward
? nextExtent.offset < _shiftTapDragSelection!.baseOffset
: nextExtent.offset > _shiftTapDragSelection!.baseOffset;
if (isInverted && selection.baseOffset == _shiftTapDragSelection!.baseOffset) {
editableText.userUpdateTextEditingValue(
editableText.textEditingValue.copyWith(
selection: TextSelection(
baseOffset: _shiftTapDragSelection!.extentOffset,
extentOffset: nextExtent.offset,
),
),
SelectionChangedCause.drag,
);
} else if (!isInverted
&& nextExtent.offset != _shiftTapDragSelection!.baseOffset
&& selection.baseOffset != _shiftTapDragSelection!.baseOffset) {
editableText.userUpdateTextEditingValue(
editableText.textEditingValue.copyWith(
selection: TextSelection(
baseOffset: _shiftTapDragSelection!.baseOffset,
extentOffset: nextExtent.offset,
),
),
SelectionChangedCause.drag,
);
} else {
_extendSelection(updateDetails.globalPosition, SelectionChangedCause.drag);
}
}
/// Handler for [TextSelectionGestureDetector.onDragSelectionEnd].
///
/// By default, it services as place holder to enable subclass override.
/// By default, it simply cleans up the state used for handling certain
/// built-in behaviors.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onDragSelectionEnd], which triggers this
/// callback.
@protected
void onDragSelectionEnd(DragEndDetails details) {/* Subclass should override this method if needed. */}
void onDragSelectionEnd(DragEndDetails details) {
if (_isShiftTapping) {
_isShiftTapping = false;
_shiftTapDragSelection = null;
}
}
/// Returns a [TextSelectionGestureDetector] configured with the handlers
/// provided by this builder.

View file

@ -5052,4 +5052,392 @@ void main() {
expect(controller.selection.baseOffset, 13);
expect(controller.selection.extentOffset, 4);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));
testWidgets('can shift + tap + drag to select with a keyboard (Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
await tester.tapAt(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 23),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Expand the selection a bit.
await gesture.moveTo(textOffsetToPosition(tester, 28));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 28);
// Move back to the original selection.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Collapse the selection.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
// Invert the selection. The base jumps to the original extent.
await gesture.moveTo(textOffsetToPosition(tester, 7));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 7);
// Continuing to move in the inverted direction expands the selection.
await gesture.moveTo(textOffsetToPosition(tester, 4));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 4);
// Move back to the original base.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Continue to move past the original base, which will cause the selection
// to invert back to the original orientation.
await gesture.moveTo(textOffsetToPosition(tester, 9));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 9);
// Continuing to select in this direction selects just like it did
// originally.
await gesture.moveTo(textOffsetToPosition(tester, 24));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
// Releasing the shift key has no effect; the selection continues as the
// mouse continues to move.
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
await gesture.moveTo(textOffsetToPosition(tester, 26));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
await gesture.up();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('can shift + tap + drag to select with a keyboard (non-Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
await tester.tapAt(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 23),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Expand the selection a bit.
await gesture.moveTo(textOffsetToPosition(tester, 28));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 28);
// Move back to the original selection.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Collapse the selection.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
// Invert the selection. The original selection is not restored like on iOS
// and Mac.
await gesture.moveTo(textOffsetToPosition(tester, 7));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 7);
// Continuing to move in the inverted direction expands the selection.
await gesture.moveTo(textOffsetToPosition(tester, 4));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 4);
// Move back to the original base.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
// Continue to move past the original base.
await gesture.moveTo(textOffsetToPosition(tester, 9));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 9);
// Continuing to select in this direction selects just like it did
// originally.
await gesture.moveTo(textOffsetToPosition(tester, 24));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
// Releasing the shift key has no effect; the selection continues as the
// mouse continues to move.
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
await gesture.moveTo(textOffsetToPosition(tester, 26));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
await gesture.up();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));
testWidgets('can shift + tap + drag to select with a keyboard, reversed (Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
// Make a selection from right to left.
await tester.tapAt(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 8),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Expand the selection a bit.
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 5);
// Move back to the original selection.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Collapse the selection.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
// Invert the selection. The base jumps to the original extent.
await gesture.moveTo(textOffsetToPosition(tester, 24));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
// Continuing to move in the inverted direction expands the selection.
await gesture.moveTo(textOffsetToPosition(tester, 27));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 27);
// Move back to the original base.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Continue to move past the original base, which will cause the selection
// to invert back to the original orientation.
await gesture.moveTo(textOffsetToPosition(tester, 22));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 22);
// Continuing to select in this direction selects just like it did
// originally.
await gesture.moveTo(textOffsetToPosition(tester, 16));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 16);
// Releasing the shift key has no effect; the selection continues as the
// mouse continues to move.
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 16);
await gesture.moveTo(textOffsetToPosition(tester, 14));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
await gesture.up();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('can shift + tap + drag to select with a keyboard, reversed (non-Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
// Make a selection from right to left.
await tester.tapAt(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 8),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Expand the selection a bit.
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 5);
// Move back to the original selection.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Collapse the selection.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
// Invert the selection. The selection is not restored like it would be on
// iOS and Mac.
await gesture.moveTo(textOffsetToPosition(tester, 24));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 24);
// Continuing to move in the inverted direction expands the selection.
await gesture.moveTo(textOffsetToPosition(tester, 27));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 27);
// Move back to the original base.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
// Continue to move past the original base.
await gesture.moveTo(textOffsetToPosition(tester, 22));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 22);
// Continuing to select in this direction selects just like it did
// originally.
await gesture.moveTo(textOffsetToPosition(tester, 16));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 16);
// Releasing the shift key has no effect; the selection continues as the
// mouse continues to move.
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 16);
await gesture.moveTo(textOffsetToPosition(tester, 14));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
await gesture.up();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));
}

View file

@ -10557,4 +10557,392 @@ void main() {
expect(controller.selection.baseOffset, 13);
expect(controller.selection.extentOffset, 4);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));
testWidgets('can shift + tap + drag to select with a keyboard (Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(controller: controller),
),
),
),
);
await tester.tapAt(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 23),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Expand the selection a bit.
await gesture.moveTo(textOffsetToPosition(tester, 28));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 28);
// Move back to the original selection.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Collapse the selection.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
// Invert the selection. The base jumps to the original extent.
await gesture.moveTo(textOffsetToPosition(tester, 7));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 7);
// Continuing to move in the inverted direction expands the selection.
await gesture.moveTo(textOffsetToPosition(tester, 4));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 4);
// Move back to the original base.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Continue to move past the original base, which will cause the selection
// to invert back to the original orientation.
await gesture.moveTo(textOffsetToPosition(tester, 9));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 9);
// Continuing to select in this direction selects just like it did
// originally.
await gesture.moveTo(textOffsetToPosition(tester, 24));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
// Releasing the shift key has no effect; the selection continues as the
// mouse continues to move.
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
await gesture.moveTo(textOffsetToPosition(tester, 26));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
await gesture.up();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('can shift + tap + drag to select with a keyboard (non-Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(controller: controller),
),
),
),
);
await tester.tapAt(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 23),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Expand the selection a bit.
await gesture.moveTo(textOffsetToPosition(tester, 28));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 28);
// Move back to the original selection.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Collapse the selection.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
// Invert the selection. The original selection is not restored like on iOS
// and Mac.
await gesture.moveTo(textOffsetToPosition(tester, 7));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 7);
// Continuing to move in the inverted direction expands the selection.
await gesture.moveTo(textOffsetToPosition(tester, 4));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 4);
// Move back to the original base.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 8);
// Continue to move past the original base.
await gesture.moveTo(textOffsetToPosition(tester, 9));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 9);
// Continuing to select in this direction selects just like it did
// originally.
await gesture.moveTo(textOffsetToPosition(tester, 24));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
// Releasing the shift key has no effect; the selection continues as the
// mouse continues to move.
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
await gesture.moveTo(textOffsetToPosition(tester, 26));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
await gesture.up();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 26);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));
testWidgets('can shift + tap + drag to select with a keyboard, reversed (Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(controller: controller),
),
),
),
);
// Make a selection from right to left.
await tester.tapAt(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 8),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Expand the selection a bit.
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 5);
// Move back to the original selection.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Collapse the selection.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
// Invert the selection. The base jumps to the original extent.
await gesture.moveTo(textOffsetToPosition(tester, 24));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 24);
// Continuing to move in the inverted direction expands the selection.
await gesture.moveTo(textOffsetToPosition(tester, 27));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 27);
// Move back to the original base.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 8);
expect(controller.selection.extentOffset, 23);
// Continue to move past the original base, which will cause the selection
// to invert back to the original orientation.
await gesture.moveTo(textOffsetToPosition(tester, 22));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 22);
// Continuing to select in this direction selects just like it did
// originally.
await gesture.moveTo(textOffsetToPosition(tester, 16));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 16);
// Releasing the shift key has no effect; the selection continues as the
// mouse continues to move.
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 16);
await gesture.moveTo(textOffsetToPosition(tester, 14));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
await gesture.up();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('can shift + tap + drag to select with a keyboard, reversed (non-Apple platforms)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(controller: controller),
),
),
),
);
// Make a selection from right to left.
await tester.tapAt(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
await tester.pump(kDoubleTapTimeout);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
final TestGesture gesture =
await tester.startGesture(
textOffsetToPosition(tester, 8),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Expand the selection a bit.
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 5);
// Move back to the original selection.
await gesture.moveTo(textOffsetToPosition(tester, 8));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 8);
// Collapse the selection.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
// Invert the selection. The selection is not restored like it would be on
// iOS and Mac.
await gesture.moveTo(textOffsetToPosition(tester, 24));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 24);
// Continuing to move in the inverted direction expands the selection.
await gesture.moveTo(textOffsetToPosition(tester, 27));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 27);
// Move back to the original base.
await gesture.moveTo(textOffsetToPosition(tester, 23));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 23);
// Continue to move past the original base.
await gesture.moveTo(textOffsetToPosition(tester, 22));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 22);
// Continuing to select in this direction selects just like it did
// originally.
await gesture.moveTo(textOffsetToPosition(tester, 16));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 16);
// Releasing the shift key has no effect; the selection continues as the
// mouse continues to move.
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 16);
await gesture.moveTo(textOffsetToPosition(tester, 14));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
await gesture.up();
expect(controller.selection.baseOffset, 23);
expect(controller.selection.extentOffset, 14);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows }));
}