Add TextField triple tap/click gestures (#119046)
Add TextField triple tap/click gestures
diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart
index d2f4fd8..fe864b8 100644
--- a/packages/flutter/lib/src/widgets/text_selection.dart
+++ b/packages/flutter/lib/src/widgets/text_selection.dart
@@ -2413,6 +2413,95 @@
}
}
+ // 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 @@
_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 @@
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 @@
);
}
+ // 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 @@
onSingleLongTapMoveUpdate: onSingleLongTapMoveUpdate,
onSingleLongTapEnd: onSingleLongTapEnd,
onDoubleTapDown: onDoubleTapDown,
+ onTripleTapDown: onTripleTapDown,
onDragSelectionStart: onDragSelectionStart,
onDragSelectionUpdate: onDragSelectionUpdate,
onDragSelectionEnd: onDragSelectionEnd,
@@ -2723,6 +2853,7 @@
this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd,
this.onDoubleTapDown,
+ this.onTripleTapDown,
this.onDragSelectionStart,
this.onDragSelectionUpdate,
this.onDragSelectionEnd,
@@ -2777,6 +2908,10 @@
/// 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 _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 @@
// 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 @@
// down event.
..dragStartBehavior = DragStartBehavior.down
..dragUpdateThrottleFrequency = _kDragSelectionUpdateThrottle
- ..maxConsecutiveTap = _getDefaultMaxConsecutiveTap()
..onTapDown = _handleTapDown
..onDragStart = _handleDragStart
..onDragUpdate = _handleDragUpdate
diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart
index 0dfbfec..47d06f0 100644
--- a/packages/flutter/test/cupertino/text_field_test.dart
+++ b/packages/flutter/test/cupertino/text_field_test.dart
@@ -2572,7 +2572,7 @@
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 @@
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 @@
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',
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index d35384f..54d24d8 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -9144,6 +9144,1250 @@
},
);
+ 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 @@
);
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 @@
// 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 @@
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),
@@ -10547,7 +11793,7 @@
pointer: 7,
kind: PointerDeviceKind.mouse,
);
- await tester.pump();
+ await tester.pump();
await gesture.up();
await tester.pump(const Duration(milliseconds: 50));
expect(
@@ -10559,7 +11805,9 @@
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 @@
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 @@
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 @@
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);
diff --git a/packages/flutter/test/widgets/text_selection_test.dart b/packages/flutter/test/widgets/text_selection_test.dart
index 937db82..f385aaa 100644
--- a/packages/flutter/test/widgets/text_selection_test.dart
+++ b/packages/flutter/test/widgets/text_selection_test.dart
@@ -18,6 +18,7 @@
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 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 @@
singleTapCancelCount = 0;
singleLongTapStartCount = 0;
doubleTapDownCount = 0;
+ tripleTapDownCount = 0;
forcePressStartCount = 0;
forcePressEndCount = 0;
dragStartCount = 0;
@@ -58,6 +61,7 @@
onSingleTapCancel: handleSingleTapCancel,
onSingleLongTapStart: handleSingleLongTapStart,
onDoubleTapDown: handleDoubleTapDown,
+ onTripleTapDown: handleTripleTapDown,
onForcePressStart: handleForcePressStart,
onForcePressEnd: handleForcePressEnd,
onDragSelectionStart: handleDragSelectionStart,
@@ -113,7 +117,7 @@
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 @@
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 {