Unverified Commit 04f6acd8 authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

Add TextField triple tap/click gestures (#119046)

Add TextField triple tap/click gestures
parent aa5db20f
......@@ -2413,6 +2413,95 @@ class TextSelectionGestureDetectorBuilder {
}
}
// Selects the set of paragraphs in a document that intersect a given range of
// global positions.
void _selectParagraphsInRange({required Offset from, Offset? to, SelectionChangedCause? cause}) {
final TextBoundary paragraphBoundary = ParagraphBoundary(editableText.textEditingValue.text);
_selectTextBoundariesInRange(boundary: paragraphBoundary, from: from, to: to, cause: cause);
}
// Selects the set of lines in a document that intersect a given range of
// global positions.
void _selectLinesInRange({required Offset from, Offset? to, SelectionChangedCause? cause}) {
final TextBoundary lineBoundary = LineBoundary(renderEditable);
_selectTextBoundariesInRange(boundary: lineBoundary, from: from, to: to, cause: cause);
}
// Returns the closest boundary location to `extent` but not including `extent`
// itself.
TextRange _moveBeyondTextBoundary(TextPosition extent, TextBoundary textBoundary) {
assert(extent.offset >= 0);
// if x is a boundary defined by `textBoundary`, most textBoundaries (except
// LineBreaker) guarantees `x == textBoundary.getLeadingTextBoundaryAt(x)`.
// Use x - 1 here to make sure we don't get stuck at the fixed point x.
final int start = textBoundary.getLeadingTextBoundaryAt(extent.offset - 1) ?? 0;
final int end = textBoundary.getTrailingTextBoundaryAt(extent.offset) ?? editableText.textEditingValue.text.length;
return TextRange(start: start, end: end);
}
// Selects the set of text boundaries in a document that intersect a given
// range of global positions.
//
// The set of text boundaries selected are not strictly bounded by the range
// of global positions.
//
// The first and last endpoints of the selection will always be at the
// beginning and end of a text boundary respectively.
void _selectTextBoundariesInRange({required TextBoundary boundary, required Offset from, Offset? to, SelectionChangedCause? cause}) {
final TextPosition fromPosition = renderEditable.getPositionForPoint(from);
final TextRange fromRange = _moveBeyondTextBoundary(fromPosition, boundary);
final TextPosition toPosition = to == null
? fromPosition
: renderEditable.getPositionForPoint(to);
final TextRange toRange = toPosition == fromPosition
? fromRange
: _moveBeyondTextBoundary(toPosition, boundary);
final bool isFromBoundaryBeforeToBoundary = fromRange.start < toRange.end;
final TextSelection newSelection = isFromBoundaryBeforeToBoundary
? TextSelection(baseOffset: fromRange.start, extentOffset: toRange.end)
: TextSelection(baseOffset: fromRange.end, extentOffset: toRange.start);
editableText.userUpdateTextEditingValue(
editableText.textEditingValue.copyWith(selection: newSelection),
cause,
);
}
/// Handler for [TextSelectionGestureDetector.onTripleTapDown].
///
/// By default, it selects a paragraph if
/// [TextSelectionGestureDetectorBuilderDelegate.selectionEnabled] is true
/// and shows the toolbar if necessary.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onTripleTapDown], which triggers this
/// callback.
@protected
void onTripleTapDown(TapDragDownDetails details) {
if (!delegate.selectionEnabled) {
return;
}
if (renderEditable.maxLines == 1) {
editableText.selectAll(SelectionChangedCause.tap);
} else {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
case TargetPlatform.macOS:
case TargetPlatform.windows:
_selectParagraphsInRange(from: details.globalPosition, cause: SelectionChangedCause.tap);
case TargetPlatform.linux:
_selectLinesInRange(from: details.globalPosition, cause: SelectionChangedCause.tap);
}
}
if (shouldShowSelectionToolbar) {
editableText.showToolbar();
}
}
/// Handler for [TextSelectionGestureDetector.onDragSelectionStart].
///
/// By default, it selects a text position specified in [details].
......@@ -2435,7 +2524,7 @@ class TextSelectionGestureDetectorBuilder {
_dragStartScrollOffset = _scrollPosition;
_dragStartViewportOffset = renderEditable.offset.pixels;
if (details.consecutiveTapCount > 1) {
if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) > 1) {
// Do not set the selection on a consecutive tap and drag.
return;
}
......@@ -2520,7 +2609,7 @@ class TextSelectionGestureDetectorBuilder {
final Offset dragStartGlobalPosition = details.globalPosition - details.offsetFromOrigin;
// Select word by word.
if (details.consecutiveTapCount == 2) {
if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) {
return renderEditable.selectWordsInRange(
from: dragStartGlobalPosition - editableOffset - scrollableOffset,
to: details.globalPosition,
......@@ -2528,6 +2617,46 @@ class TextSelectionGestureDetectorBuilder {
);
}
// Select paragraph-by-paragraph.
if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 3) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
switch (details.kind) {
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
return _selectParagraphsInRange(
from: dragStartGlobalPosition - editableOffset - scrollableOffset,
to: details.globalPosition,
cause: SelectionChangedCause.drag,
);
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
case null:
// Triple tap to drag is not present on these platforms when using
// non-precise pointer devices at the moment.
break;
}
return;
case TargetPlatform.linux:
return _selectLinesInRange(
from: dragStartGlobalPosition - editableOffset - scrollableOffset,
to: details.globalPosition,
cause: SelectionChangedCause.drag,
);
case TargetPlatform.windows:
case TargetPlatform.macOS:
return _selectParagraphsInRange(
from: dragStartGlobalPosition - editableOffset - scrollableOffset,
to: details.globalPosition,
cause: SelectionChangedCause.drag,
);
}
}
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
// With a touch device, nothing should happen, unless there was a double tap, or
......@@ -2684,6 +2813,7 @@ class TextSelectionGestureDetectorBuilder {
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
onSingleLongTapEnd: onSingleLongTapEnd,
onDoubleTapDown: onDoubleTapDown,
onTripleTapDown: onTripleTapDown,
onDragSelectionStart: onDragSelectionStart,
onDragSelectionUpdate: onDragSelectionUpdate,
onDragSelectionEnd: onDragSelectionEnd,
......@@ -2723,6 +2853,7 @@ class TextSelectionGestureDetector extends StatefulWidget {
this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd,
this.onDoubleTapDown,
this.onTripleTapDown,
this.onDragSelectionStart,
this.onDragSelectionUpdate,
this.onDragSelectionEnd,
......@@ -2777,6 +2908,10 @@ class TextSelectionGestureDetector extends StatefulWidget {
/// time (within [kDoubleTapTimeout]) to a previous short tap.
final GestureTapDragDownCallback? onDoubleTapDown;
/// Called after a momentary hold or a short tap that is close in space and
/// time (within [kDoubleTapTimeout]) to a previous double-tap.
final GestureTapDragDownCallback? onTripleTapDown;
/// Called when a mouse starts dragging to select text.
final GestureTapDragStartCallback? onDragSelectionStart;
......@@ -2803,7 +2938,42 @@ class TextSelectionGestureDetector extends StatefulWidget {
}
class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetector> {
static int? _getDefaultMaxConsecutiveTap() => 2;
// Converts the details.consecutiveTapCount from a TapAndDrag*Details object,
// which can grow to be infinitely large, to a value between 1 and 3. The value
// that the raw count is converted to is based on the default observed behavior
// on the native platforms.
//
// This method should be used in all instances when details.consecutiveTapCount
// would be used.
static int _getEffectiveConsecutiveTapCount(int rawCount) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
// From observation, these platform's reset their tap count to 0 when
// the number of consecutive taps exceeds 3. For example on Debian Linux
// with GTK, when going past a triple click, on the fourth click the
// selection is moved to the precise click position, on the fifth click
// the word at the position is selected, and on the sixth click the
// paragraph at the position is selected.
return rawCount <= 3 ? rawCount : (rawCount % 3 == 0 ? 3 : rawCount % 3);
case TargetPlatform.iOS:
case TargetPlatform.macOS:
// From observation, these platform's either hold their tap count at 3.
// For example on macOS, when going past a triple click, the selection
// should be retained at the paragraph that was first selected on triple
// click.
return math.min(rawCount, 3);
case TargetPlatform.windows:
// From observation, this platform's consecutive tap actions alternate
// between double click and triple click actions. For example, after a
// triple click has selected a paragraph, on the next click the word at
// the clicked position will be selected, and on the next click the
// paragraph at the position is selected.
return rawCount < 2 ? rawCount : 2 + rawCount % 2;
}
}
@override
void dispose() {
......@@ -2818,14 +2988,17 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
// because it's 2 single taps, each of which may do different things depending
// on whether it's a single tap, the first tap of a double tap, the second
// tap held down, a clean double tap etc.
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) {
return widget.onDoubleTapDown?.call(details);
}
if (details.consecutiveTapCount == 2) {
widget.onDoubleTapDown?.call(details);
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 3) {
return widget.onTripleTapDown?.call(details);
}
}
void _handleTapUp(TapDragUpDetails details) {
if (details.consecutiveTapCount == 1) {
if (_getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 1) {
widget.onSingleTapUp?.call(details);
}
}
......@@ -2910,7 +3083,6 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
// down event.
..dragStartBehavior = DragStartBehavior.down
..dragUpdateThrottleFrequency = _kDragSelectionUpdateThrottle
..maxConsecutiveTap = _getDefaultMaxConsecutiveTap()
..onTapDown = _handleTapDown
..onDragStart = _handleDragStart
..onDragUpdate = _handleDragUpdate
......
......@@ -2572,7 +2572,7 @@ void main() {
await gesture.down(textOffsetToPosition(tester, 5));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 5);
expect(controller.value.selection.extentOffset, 6);
......@@ -3374,7 +3374,9 @@ void main() {
expect(find.byType(CupertinoButton), findsNothing);
// Second tap shows the toolbar, and retains the selection.
await tester.tapAt(textFieldStart + const Offset(100.0, 5.0));
await tester.pumpAndSettle();
// Wait for the consecutive tap timer to timeout so the next
// tap is not detected as a triple tap.
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
......@@ -3398,6 +3400,1259 @@ void main() {
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
group('Triple tap/click', () {
const String testValueA = 'Now is the time for\n' // 20
'all good people\n' // 20 + 16 => 36
'to come to the aid\n' // 36 + 19 => 55
'of their country.'; // 55 + 17 => 72
const String testValueB = 'Today is the time for\n' // 22
'all good people\n' // 22 + 16 => 38
'to come to the aid\n' // 38 + 19 => 57
'of their country.'; // 57 + 17 => 74
testWidgets(
'Can triple tap to select a paragraph on mobile platforms when tapping at a word edge',
(WidgetTester tester) async {
// TODO(Renzo-Olivares): Enable, currently broken because selection overlay blocks the TextSelectionGestureDetector.
final TextEditingController controller = TextEditingController();
final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueA);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueA);
final Offset firstLinePos = tester.getTopLeft(find.byType(CupertinoTextField)) + const Offset(110.0, 9.0);
// Tap on text field to gain focus, and set selection to 'is|' on the first line.
final TestGesture gesture = await tester.startGesture(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 6);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position. On iOS, tapping a whitespace selects the previous word.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 6);
expect(controller.selection.extentOffset, isTargetPlatformApple ? 6 : 7);
// Here we tap on same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 20);
},
variant: TargetPlatformVariant.mobile(),
skip: true, // https://github.com/flutter/flutter/issues/123415
);
testWidgets(
'Can triple tap to select a paragraph on mobile platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueB);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueB);
final Offset firstLinePos = tester.getTopLeft(find.byType(CupertinoTextField)) + const Offset(50.0, 9.0);
// Tap on text field to gain focus, and move the selection.
final TestGesture gesture = await tester.startGesture(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 5);
// Here we tap on same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 22);
},
variant: TargetPlatformVariant.mobile(),
);
testWidgets(
'triple tap chains work on Non-Apple mobile platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 3);
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 35),
);
// Triple tap selecting the same paragraph somewhere else is fine.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap hides the toolbar and moves the selection.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 6);
expect(find.byType(CupertinoButton), findsNothing);
// Second tap shows the toolbar and selects the word.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
// Third tap shows the toolbar and selects the paragraph.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 35),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor and hid the toolbar.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 9);
expect(find.byType(CupertinoButton), findsNothing);
// Second tap selects the word.
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
// Third tap selects the paragraph and shows the toolbar.
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 35),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }),
);
testWidgets(
'triple tap chains work on Apple platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure\nThe fox jumped over the fence.',
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 7);
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 36),
);
// Triple tap selecting the same paragraph somewhere else is fine.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap hides the toolbar and retains the selection.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 36),
);
expect(find.byType(CupertinoButton), findsNothing);
// Second tap shows the toolbar and selects the word.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
// Third tap shows the toolbar and selects the paragraph.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 36),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
await tester.tapAt(textfieldStart + const Offset(150.0, 25.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor and hid the toolbar.
expect(
controller.selection,
const TextSelection.collapsed(offset: 50, affinity: TextAffinity.upstream),
);
expect(find.byType(CupertinoButton), findsNothing);
// Second tap selects the word.
await tester.tapAt(textfieldStart + const Offset(150.0, 25.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 44, extentOffset: 50),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
// Third tap selects the paragraph and shows the toolbar.
await tester.tapAt(textfieldStart + const Offset(150.0, 25.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 36, extentOffset: 66),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets(
'triple click chains work',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;
// First click moves the cursor to the point of the click, not the edge of
// the clicked word.
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(200.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 12);
// Second click selects the word.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
// Triple click selects the paragraph.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
// Wait for the consecutive tap timer to timeout so the next
// tap is not detected as a triple tap.
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
// Triple click selecting the same paragraph somewhere else is fine.
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// First click moved the cursor.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 6);
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Second click selected the word.
expect(
controller.selection,
const TextSelection(baseOffset: 4, extentOffset: 6),
);
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
await tester.pump();
await gesture.up();
// Wait for the consecutive tap timer to timeout so the tap count
// is reset.
await tester.pumpAndSettle(kDoubleTapTimeout);
// Third click selected the paragraph.
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// First click moved the cursor.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 9);
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Second click selected the word.
expect(
controller.selection,
const TextSelection(baseOffset: 7, extentOffset: 10),
);
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Third click selects the paragraph.
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
},
variant: TargetPlatformVariant.desktop(),
);
testWidgets(
'triple click after a click on desktop platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(50.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 3);
// First click moves the selection.
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 9);
// Double click selection to select a word.
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 7, extentOffset: 10),
);
// Triple click selection to select a paragraph.
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
},
variant: TargetPlatformVariant.desktop(),
);
testWidgets(
'Can triple tap to select all on a single-line textfield on mobile platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueB,
);
final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
controller: controller,
),
),
),
);
final Offset firstLinePos = tester.getTopLeft(find.byType(CupertinoTextField)) + const Offset(50.0, 9.0);
// Tap on text field to gain focus, and set selection somewhere on the first word.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 5);
// Here we tap on same position again, to register a triple tap. This will select
// the entire text field if it is a single-line field.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 74);
},
variant: TargetPlatformVariant.mobile(),
);
testWidgets(
'Can triple click to select all on a single-line textfield on desktop platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on same position again, to register a triple tap. This will select
// the entire text field if it is a single-line field.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 72);
},
variant: TargetPlatformVariant.desktop(),
);
testWidgets(
'Can triple click to select a line on Linux',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueA);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueA);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 19);
},
variant: TargetPlatformVariant.only(TargetPlatform.linux),
);
testWidgets(
'Can triple click to select a paragraph',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueA);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueA);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 20);
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }),
);
testWidgets(
'Can triple click + drag to select line by line on Linux',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueA);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueA);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on the same position again, to register a triple tap. This will select
// the line at the tapped position.
await gesture.down(firstLinePos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 19);
// Drag, down after the triple tap, to select line by line.
// Moving down will extend the selection to the second line.
await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 35);
// Moving down will extend the selection to the third line.
await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 54);
// Moving down will extend the selection to the last line.
await gesture.moveTo(firstLinePos + const Offset(0, 40.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 72);
// Moving up will extend the selection to the third line.
await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 54);
// Moving up will extend the selection to the second line.
await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 35);
// Moving up will extend the selection to the first line.
await gesture.moveTo(firstLinePos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 19);
},
variant: TargetPlatformVariant.only(TargetPlatform.linux),
);
testWidgets(
'Can triple click + drag to select paragraph by paragraph',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(CupertinoTextField), testValueA);
// Skip past scrolling animation.
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
expect(controller.value.text, testValueA);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on the same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 20);
// Drag, down after the triple tap, to select paragraph by paragraph.
// Moving down will extend the selection to the second line.
await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 36);
// Moving down will extend the selection to the third line.
await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 55);
// Moving down will extend the selection to the last line.
await gesture.moveTo(firstLinePos + const Offset(0, 40.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 72);
// Moving up will extend the selection to the third line.
await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 55);
// Moving up will extend the selection to the second line.
await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 36);
// Moving up will extend the selection to the first line.
await gesture.moveTo(firstLinePos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 20);
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }),
);
testWidgets(
'Going past triple click retains the selection on Apple platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
// First click moves the cursor to the point of the click, not the edge of
// the clicked word.
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(200.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 12);
// Second click selects the word.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
// Triple click selects the paragraph.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
// Clicking again retains the selection.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again retains the selection.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again retains the selection.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'Tap count resets when going past a triple tap on Android, Fuchsia, and Linux',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;
// First click moves the cursor to the point of the click, not the edge of
// the clicked word.
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(200.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 12);
// Second click selects the word.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
// Triple click selects the paragraph.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
// Clicking again moves the caret to the tapped positio.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 12);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again selects the word.
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again selects the paragraph.
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// Clicking again moves the caret to the tapped position.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 12);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again selects the word.
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again selects the paragraph.
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux }),
);
testWidgets(
'Double click and triple click alternate on Windows',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: Center(
child: CupertinoTextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
// First click moves the cursor to the point of the click, not the edge of
// the clicked word.
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(200.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 12);
// Second click selects the word.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
// Triple click selects the paragraph.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
// Clicking again selects the word.
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again selects the paragraph.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again selects the word.
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// Clicking again selects the paragraph.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again selects the word.
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(200.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again selects the paragraph.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
},
variant: TargetPlatformVariant.only(TargetPlatform.windows),
);
});
testWidgets('force press selects word', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
......
......@@ -9144,6 +9144,1250 @@ void main() {
},
);
group('Triple tap/click', () {
const String testValueA = 'Now is the time for\n' // 20
'all good people\n' // 20 + 16 => 36
'to come to the aid\n' // 36 + 19 => 55
'of their country.'; // 55 + 17 => 72
const String testValueB = 'Today is the time for\n' // 22
'all good people\n' // 22 + 16 => 38
'to come to the aid\n' // 38 + 19 => 57
'of their country.'; // 57 + 17 => 74
testWidgets(
'Can triple tap to select a paragraph on mobile platforms when tapping at a word edge',
(WidgetTester tester) async {
// TODO(Renzo-Olivares): Enable for iOS, currently broken because selection overlay blocks the TextSelectionGestureDetector https://github.com/flutter/flutter/issues/123415.
final TextEditingController controller = TextEditingController();
final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(TextField), testValueA);
await skipPastScrollingAnimation(tester);
expect(controller.value.text, testValueA);
final Offset firstLinePos = textOffsetToPosition(tester, 6);
// Tap on text field to gain focus, and set selection to 'is|' on the first line.
final TestGesture gesture = await tester.startGesture(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 6);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 6);
expect(controller.selection.extentOffset, isTargetPlatformApple ? 6 : 7);
// Here we tap on same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 20);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }),
);
testWidgets(
'Can triple tap to select a paragraph on mobile platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(TextField), testValueB);
await skipPastScrollingAnimation(tester);
expect(controller.value.text, testValueB);
final Offset firstLinePos = tester.getTopLeft(find.byType(TextField)) + const Offset(50.0, 9.0);
// Tap on text field to gain focus, and move the selection.
final TestGesture gesture = await tester.startGesture(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 5);
// Here we tap on same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 22);
},
variant: TargetPlatformVariant.mobile(),
);
testWidgets(
'triple tap chains work on Non-Apple mobile 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,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 3);
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 35),
);
// Triple tap selecting the same paragraph somewhere else is fine.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap hides the toolbar and moves the selection.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 6);
expect(find.byType(TextButton), findsNothing);
// Second tap shows the toolbar and selects the word.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
// Third tap shows the toolbar and selects the paragraph.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 35),
);
expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor and hid the toolbar.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 9);
expect(find.byType(TextButton), findsNothing);
// Second tap selects the word.
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 8, extentOffset: 12),
);
expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
// Third tap selects the paragraph and shows the toolbar.
await tester.tapAt(textfieldStart + const Offset(150.0, 9.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 35),
);
expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia }),
);
testWidgets(
'triple tap chains work on Apple platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure\nThe fox jumped over the fence.',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 7);
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
await tester.tapAt(textfieldStart + const Offset(50.0, 9.0));
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 36),
);
// Triple tap selecting the same paragraph somewhere else is fine.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap hides the toolbar and retains the selection.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 36),
);
expect(find.byType(CupertinoButton), findsNothing);
// Second tap shows the toolbar and selects the word.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
// Third tap shows the toolbar and selects the paragraph.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 36),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
await tester.tapAt(textfieldStart + const Offset(150.0, 50.0));
await tester.pump(const Duration(milliseconds: 50));
// First tap moved the cursor and hid the toolbar.
expect(
controller.selection,
const TextSelection.collapsed(offset: 50, affinity: TextAffinity.upstream),
);
expect(find.byType(CupertinoButton), findsNothing);
// Second tap selects the word.
await tester.tapAt(textfieldStart + const Offset(150.0, 50.0));
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 44, extentOffset: 50),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
// Third tap selects the paragraph and shows the toolbar.
await tester.tapAt(textfieldStart + const Offset(150.0, 50.0));
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 36, extentOffset: 66),
);
expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3));
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets(
'triple click chains work',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;
// First click moves the cursor to the point of the click, not the edge of
// the clicked word.
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(210.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 13);
// Second click selects the word.
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
// Triple click selects the paragraph.
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
// Wait for the consecutive tap timer to timeout so the next
// tap is not detected as a triple tap.
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
// Triple click selecting the same paragraph somewhere else is fine.
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// First click moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 6),
);
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Second click selected the word.
expect(
controller.selection,
const TextSelection(baseOffset: 6, extentOffset: 7),
);
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
await tester.pump();
await gesture.up();
// Wait for the consecutive tap timer to timeout so the tap count
// is reset.
await tester.pumpAndSettle(kDoubleTapTimeout);
// Third click selected the paragraph.
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// First click moved the cursor.
expect(
controller.selection,
const TextSelection.collapsed(offset: 9),
);
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Second click selected the word.
expect(
controller.selection,
const TextSelection(baseOffset: 7, extentOffset: 10),
);
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Third click selects the paragraph.
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
},
variant: TargetPlatformVariant.desktop(),
);
testWidgets(
'triple click after a click on desktop platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(50.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
const TextSelection.collapsed(offset: 3),
);
// First click moves the selection.
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(
controller.selection,
const TextSelection.collapsed(offset: 9),
);
// Double click selection to select a word.
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 7, extentOffset: 10),
);
// Triple click selection to select a paragraph.
await gesture.down(textFieldStart + const Offset(150.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
},
variant: TargetPlatformVariant.desktop(),
);
testWidgets(
'Can triple tap to select all on a single-line textfield on mobile platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueB,
);
final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
controller: controller,
),
),
),
);
final Offset firstLinePos = tester.getTopLeft(find.byType(TextField)) + const Offset(50.0, 9.0);
// Tap on text field to gain focus, and set selection somewhere on the first word.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, isTargetPlatformApple ? 5 : 3);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 5);
// Here we tap on same position again, to register a triple tap. This will select
// the entire text field if it is a single-line field.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 74);
},
variant: TargetPlatformVariant.mobile(),
);
testWidgets(
'Can triple click to select all on a single-line textfield on desktop platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on same position again, to register a triple tap. This will select
// the entire text field if it is a single-line field.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 72);
},
variant: TargetPlatformVariant.desktop(),
);
testWidgets(
'Can triple click to select a line on Linux',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(TextField), testValueA);
await skipPastScrollingAnimation(tester);
expect(controller.value.text, testValueA);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 19);
},
variant: TargetPlatformVariant.only(TargetPlatform.linux),
);
testWidgets(
'Can triple click to select a paragraph',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(TextField), testValueA);
await skipPastScrollingAnimation(tester);
expect(controller.value.text, testValueA);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 20);
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }),
);
testWidgets(
'Can triple click + drag to select line by line on Linux',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(TextField), testValueA);
await skipPastScrollingAnimation(tester);
expect(controller.value.text, testValueA);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on the same position again, to register a triple tap. This will select
// the line at the tapped position.
await gesture.down(firstLinePos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 19);
// Drag, down after the triple tap, to select line by line.
// Moving down will extend the selection to the second line.
await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 35);
// Moving down will extend the selection to the third line.
await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 54);
// Moving down will extend the selection to the last line.
await gesture.moveTo(firstLinePos + const Offset(0, 40.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 72);
// Moving up will extend the selection to the third line.
await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 54);
// Moving up will extend the selection to the second line.
await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 35);
// Moving up will extend the selection to the first line.
await gesture.moveTo(firstLinePos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 19);
},
variant: TargetPlatformVariant.only(TargetPlatform.linux),
);
testWidgets(
'Can triple click + drag to select paragraph by paragraph',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
maxLines: null,
),
),
),
);
await tester.enterText(find.byType(TextField), testValueA);
await skipPastScrollingAnimation(tester);
expect(controller.value.text, testValueA);
final Offset firstLinePos = textOffsetToPosition(tester, 5);
// Tap on text field to gain focus, and set selection to 'i|s' on the first line.
final TestGesture gesture = await tester.startGesture(
firstLinePos,
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
// Here we tap on same position again, to register a double tap. This will select
// the word at the tapped position.
await gesture.down(firstLinePos);
await tester.pump();
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 6);
// Here we tap on the same position again, to register a triple tap. This will select
// the paragraph at the tapped position.
await gesture.down(firstLinePos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 20);
// Drag, down after the triple tap, to select paragraph by paragraph.
// Moving down will extend the selection to the second line.
await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 36);
// Moving down will extend the selection to the third line.
await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 55);
// Moving down will extend the selection to the last line.
await gesture.moveTo(firstLinePos + const Offset(0, 40.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 72);
// Moving up will extend the selection to the third line.
await gesture.moveTo(firstLinePos + const Offset(0, 20.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 55);
// Moving up will extend the selection to the second line.
await gesture.moveTo(firstLinePos + const Offset(0, 10.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 36);
// Moving up will extend the selection to the first line.
await gesture.moveTo(firstLinePos);
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 20);
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.linux }),
);
testWidgets(
'Going past triple click retains the selection on Apple platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
// First click moves the cursor to the point of the click, not the edge of
// the clicked word.
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(210.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 13);
// Second click selects the word.
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
// Triple click selects the paragraph.
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
// Clicking again retains the selection.
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again retains the selection.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again retains the selection.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
);
testWidgets(
'Tap count resets when going past a triple tap on Android, Fuchsia, and Linux',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
final bool platformSelectsByLine = defaultTargetPlatform == TargetPlatform.linux;
// First click moves the cursor to the point of the click, not the edge of
// the clicked word.
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(210.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 13);
// Second click selects the word.
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
// Triple click selects the paragraph.
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
// Clicking again moves the caret to the tapped positio.
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 13);
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again selects the word.
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again selects the paragraph.
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// Clicking again moves the caret to the tapped position.
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 13);
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again selects the word.
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again selects the paragraph.
expect(
controller.selection,
TextSelection(baseOffset: 0, extentOffset: platformSelectsByLine ? 19 : 20),
);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux }),
);
testWidgets(
'Double click and triple click alternate on Windows',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: testValueA,
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
maxLines: null,
),
),
),
),
);
final Offset textFieldStart = tester.getTopLeft(find.byType(TextField));
// First click moves the cursor to the point of the click, not the edge of
// the clicked word.
final TestGesture gesture = await tester.startGesture(
textFieldStart + const Offset(210.0, 9.0),
pointer: 7,
kind: PointerDeviceKind.mouse,
);
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 13);
// Second click selects the word.
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
// Triple click selects the paragraph.
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
// Clicking again selects the word.
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again selects the paragraph.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again selects the word.
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
// Clicking again selects the paragraph.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pump();
// Clicking again selects the word.
expect(
controller.selection,
const TextSelection(baseOffset: 11, extentOffset: 15),
);
await gesture.down(textFieldStart + const Offset(210.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Clicking again selects the paragraph.
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 20),
);
},
variant: TargetPlatformVariant.only(TargetPlatform.windows),
);
});
testWidgets(
'double tap on top of cursor also selects word',
(WidgetTester tester) async {
......@@ -10391,7 +11635,7 @@ void main() {
);
testWidgets(
'double click after a click on Mac',
'double click after a click on desktop platforms',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
......@@ -10446,7 +11690,7 @@ void main() {
// The text selection toolbar isn't shown on Mac without a right click.
expect(find.byType(CupertinoButton), findsNothing);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux }),
variant: TargetPlatformVariant.desktop(),
);
testWidgets(
......@@ -10494,7 +11738,9 @@ void main() {
expect(find.byType(CupertinoButton), findsNothing);
// Second tap shows the toolbar and retains the selection.
await tester.tapAt(textfieldStart + const Offset(100.0, 9.0));
await tester.pumpAndSettle();
// Wait for the consecutive tap timer to timeout so the next
// tap is not detected as a triple tap.
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
......@@ -10559,7 +11805,9 @@ void main() {
await gesture.down(textFieldStart + const Offset(50.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Wait for the consecutive tap timer to timeout so the next
// tap is not detected as a triple tap.
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
......@@ -10579,7 +11827,9 @@ void main() {
await gesture.down(textFieldStart + const Offset(100.0, 9.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Wait for the consecutive tap timer to timeout so the next
// tap is not detected as a triple tap.
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(
controller.selection,
const TextSelection(baseOffset: 0, extentOffset: 7),
......@@ -10734,7 +11984,7 @@ void main() {
expect(controller.value.selection.extentOffset, 1);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia, TargetPlatform.android }));
testWidgets('selecting a space selects the space on Mac', (WidgetTester tester) async {
testWidgets('selecting a space selects the space on Desktop platforms', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: ' blah blah',
);
......@@ -10775,7 +12025,9 @@ void main() {
await gesture.down(textOffsetToPosition(tester, 5));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
// Wait for the consecutive tap timer to timeout so our next tap is not
// detected as a triple tap.
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.value.selection, isNotNull);
expect(controller.value.selection.baseOffset, 5);
expect(controller.value.selection.extentOffset, 6);
......
......@@ -18,6 +18,7 @@ void main() {
late int singleTapCancelCount;
late int singleLongTapStartCount;
late int doubleTapDownCount;
late int tripleTapDownCount;
late int forcePressStartCount;
late int forcePressEndCount;
late int dragStartCount;
......@@ -30,6 +31,7 @@ void main() {
void handleSingleTapCancel() { singleTapCancelCount++; }
void handleSingleLongTapStart(LongPressStartDetails details) { singleLongTapStartCount++; }
void handleDoubleTapDown(TapDragDownDetails details) { doubleTapDownCount++; }
void handleTripleTapDown(TapDragDownDetails details) { tripleTapDownCount++; }
void handleForcePressStart(ForcePressDetails details) { forcePressStartCount++; }
void handleForcePressEnd(ForcePressDetails details) { forcePressEndCount++; }
void handleDragSelectionStart(TapDragStartDetails details) { dragStartCount++; }
......@@ -42,6 +44,7 @@ void main() {
singleTapCancelCount = 0;
singleLongTapStartCount = 0;
doubleTapDownCount = 0;
tripleTapDownCount = 0;
forcePressStartCount = 0;
forcePressEndCount = 0;
dragStartCount = 0;
......@@ -58,6 +61,7 @@ void main() {
onSingleTapCancel: handleSingleTapCancel,
onSingleLongTapStart: handleSingleLongTapStart,
onDoubleTapDown: handleDoubleTapDown,
onTripleTapDown: handleTripleTapDown,
onForcePressStart: handleForcePressStart,
onForcePressEnd: handleForcePressEnd,
onDragSelectionStart: handleDragSelectionStart,
......@@ -113,7 +117,7 @@ void main() {
expect(tapCount, 6);
});
testWidgets('in a series of rapid taps, onTapDown and onDoubleTapDown alternate', (WidgetTester tester) async {
testWidgets('in a series of rapid taps, onTapDown, onDoubleTapDown, and onTripleTapDown alternate', (WidgetTester tester) async {
await pumpGestureDetector(tester);
await tester.tapAt(const Offset(200, 200));
await tester.pump(const Duration(milliseconds: 50));
......@@ -124,20 +128,29 @@ void main() {
expect(doubleTapDownCount, 1);
await tester.tapAt(const Offset(200, 200));
await tester.pump(const Duration(milliseconds: 50));
expect(singleTapUpCount, 1);
expect(doubleTapDownCount, 1);
expect(tripleTapDownCount, 1);
await tester.tapAt(const Offset(200, 200));
await tester.pump(const Duration(milliseconds: 50));
expect(singleTapUpCount, 2);
expect(doubleTapDownCount, 1);
expect(tripleTapDownCount, 1);
await tester.tapAt(const Offset(200, 200));
await tester.pump(const Duration(milliseconds: 50));
expect(singleTapUpCount, 2);
expect(doubleTapDownCount, 2);
expect(tripleTapDownCount, 1);
await tester.tapAt(const Offset(200, 200));
await tester.pump(const Duration(milliseconds: 50));
expect(singleTapUpCount, 3);
expect(singleTapUpCount, 2);
expect(doubleTapDownCount, 2);
expect(tripleTapDownCount, 2);
await tester.tapAt(const Offset(200, 200));
expect(singleTapUpCount, 3);
expect(doubleTapDownCount, 3);
expect(tapCount, 6);
expect(doubleTapDownCount, 2);
expect(tripleTapDownCount, 2);
expect(tapCount, 7);
});
testWidgets('quick tap-tap-hold is a double tap down', (WidgetTester tester) async {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment