diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index 410aa3dac32c..f0d12c88d546 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -1454,6 +1454,9 @@ class TextSelectionGestureDetectorBuilder { /// * [TextSelectionGestureDetector.onTapDown], which triggers this callback. @protected void onTapDown(TapDownDetails details) { + if (!delegate.selectionEnabled) { + return; + } renderEditable.handleTapDown(details); // The selection overlay should only be shown when the user is interacting // through a touch screen (via either a finger or a stylus). A mouse shouldn't @@ -1465,13 +1468,21 @@ class TextSelectionGestureDetectorBuilder { || kind == PointerDeviceKind.stylus; // Handle shift + click selection if needed. - if (_isShiftPressed && renderEditable.selection?.baseOffset != null) { - _isShiftTapping = true; - switch (defaultTargetPlatform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: - // On these platforms, a shift-tapped unfocused field expands from 0, - // not from the previous selection. + final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null; + switch (defaultTargetPlatform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.iOS: + // On mobile platforms the selection is set on tap up. + if (_isShiftTapping) { + _isShiftTapping = false; + } + break; + case TargetPlatform.macOS: + // On macOS, a shift-tapped unfocused field expands from 0, not from the + // previous selection. + if (isShiftPressedValid) { + _isShiftTapping = true; final TextSelection? fromSelection = renderEditable.hasFocus ? null : const TextSelection.collapsed(offset: 0); @@ -1480,14 +1491,23 @@ class TextSelectionGestureDetectorBuilder { SelectionChangedCause.tap, fromSelection, ); - break; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: + return; + } + // On macOS, a tap/click places the selection in a precise position. + // This differs from iOS/iPadOS, where if the gesture is done by a touch + // then the selection moves to the closest word edge, instead of a + // precise position. + renderEditable.selectPosition(cause: SelectionChangedCause.tap); + break; + case TargetPlatform.linux: + case TargetPlatform.windows: + if (isShiftPressedValid) { + _isShiftTapping = true; _extendSelection(details.globalPosition, SelectionChangedCause.tap); - break; - } + return; + } + renderEditable.selectPosition(cause: SelectionChangedCause.tap); + break; } } @@ -1547,15 +1567,42 @@ class TextSelectionGestureDetectorBuilder { /// this callback. @protected void onSingleTapUp(TapUpDetails details) { - if (_isShiftTapping) { - _isShiftTapping = false; - return; - } - if (delegate.selectionEnabled) { + // Handle shift + click selection if needed. + final bool isShiftPressedValid = _isShiftPressed && renderEditable.selection?.baseOffset != null; switch (defaultTargetPlatform) { - case TargetPlatform.iOS: + case TargetPlatform.linux: case TargetPlatform.macOS: + case TargetPlatform.windows: + // On desktop platforms the selection is set on tap down. + if (_isShiftTapping) { + _isShiftTapping = false; + } + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + if (isShiftPressedValid) { + _isShiftTapping = true; + _extendSelection(details.globalPosition, SelectionChangedCause.tap); + return; + } + renderEditable.selectPosition(cause: SelectionChangedCause.tap); + break; + case TargetPlatform.iOS: + if (isShiftPressedValid) { + // On iOS, a shift-tapped unfocused field expands from 0, not from + // the previous selection. + _isShiftTapping = true; + final TextSelection? fromSelection = renderEditable.hasFocus + ? null + : const TextSelection.collapsed(offset: 0); + _expandSelection( + details.globalPosition, + SelectionChangedCause.tap, + fromSelection, + ); + return; + } switch (details.kind) { case PointerDeviceKind.mouse: case PointerDeviceKind.trackpad: @@ -1566,18 +1613,11 @@ class TextSelectionGestureDetectorBuilder { break; case PointerDeviceKind.touch: case PointerDeviceKind.unknown: - // On macOS/iOS/iPadOS a touch tap places the cursor at the edge - // of the word. + // On iOS/iPadOS a touch tap places the cursor at the edge of the word. renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); break; } break; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - renderEditable.selectPosition(cause: SelectionChangedCause.tap); - break; } } } diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index 7b058980c082..c4c445f1d130 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -1755,7 +1755,7 @@ void main() { // But don't trigger the toolbar. expect(find.byType(CupertinoButton), findsNothing); - }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); + }, variant: const TargetPlatformVariant({ TargetPlatform.iOS })); testWidgets( 'slow double tap does not trigger double tap', @@ -1763,6 +1763,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -1773,18 +1776,16 @@ void main() { ), ); - final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + final Offset pos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'. - await tester.tapAt(textFieldStart + const Offset(50.0, 5.0)); + await tester.tapAt(pos); await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(textFieldStart + const Offset(50.0, 5.0)); + await tester.tapAt(pos); await tester.pump(); // Plain collapsed selection. - expect( - controller.selection, - const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), - ); + expect(controller.selection.isCollapsed, isTrue); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6); // No toolbar. expect(find.byType(CupertinoButton), findsNothing); @@ -1893,6 +1894,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -1903,19 +1907,20 @@ void main() { ), ); - final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'. + final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'. - await tester.tapAt(textFieldStart + const Offset(50.0, 5.0)); + + await tester.tapAt(ePos); await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(textFieldStart + const Offset(150.0, 5.0)); + await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor. - expect( - controller.selection, - const TextSelection.collapsed(offset: 8), - ); - await tester.tapAt(textFieldStart + const Offset(150.0, 5.0)); + expect(controller.selection.isCollapsed, isTrue); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9); + + await tester.tapAt(pPos); await tester.pumpAndSettle(); // Second tap selects the word around the cursor. @@ -1979,6 +1984,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -1989,29 +1997,27 @@ void main() { ), ); - final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'. + final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r' - await tester.tapAt(textFieldStart + const Offset(150.0, 5.0)); + await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor. - expect( - controller.selection, - const TextSelection.collapsed(offset: 8), - ); - await tester.tapAt(textFieldStart + const Offset(150.0, 5.0)); + expect(controller.selection.isCollapsed, isTrue); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9); + + await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(textFieldStart + const Offset(100.0, 5.0)); + await tester.tapAt(ePos); await tester.pump(); // Plain collapsed selection at the edge of first word. In iOS 12, the // first tap after a double tap ends up putting the cursor at where // you tapped instead of the edge like every other single tap. This is // likely a bug in iOS 12 and not present in other versions. - expect( - controller.selection, - const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), - ); + expect(controller.selection.isCollapsed, isTrue); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6); // No toolbar. expect(find.byType(CupertinoButton), findsNothing); @@ -2429,6 +2435,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -2439,20 +2448,18 @@ void main() { ), ); - final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r' - await tester.longPressAt(textFieldStart + const Offset(50.0, 5.0)); + await tester.longPressAt(ePos); await tester.pump(const Duration(milliseconds: 50)); - await tester.tapAt(textFieldStart + const Offset(50.0, 5.0)); + await tester.tapAt(ePos); await tester.pump(); // We ended up moving the cursor to the edge of the same word and dismissed // the toolbar. - expect( - controller.selection, - const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), - ); + expect(controller.selection.isCollapsed, isTrue); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6); // The toolbar from the long press is now dismissed by the second tap. expect(find.byType(CupertinoButton), findsNothing); @@ -2615,6 +2622,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -2625,25 +2635,24 @@ void main() { ), ); - final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel' + final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r' - await tester.tapAt(textFieldStart + const Offset(150.0, 5.0)); + await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor to the beginning of the second word. - expect( - controller.selection, - const TextSelection.collapsed(offset: 8), - ); - await tester.tapAt(textFieldStart + const Offset(150.0, 5.0)); + expect(controller.selection.isCollapsed, isTrue); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9); + await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 500)); - await tester.longPressAt(textFieldStart + const Offset(100.0, 5.0)); + await tester.longPressAt(ePos); await tester.pumpAndSettle(); // Plain collapsed selection at the exact tap position. expect( controller.selection, - const TextSelection.collapsed(offset: 6, affinity: TextAffinity.upstream), + const TextSelection.collapsed(offset: 6), ); // Long press toolbar. @@ -2656,6 +2665,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -2666,19 +2678,18 @@ void main() { ), ); - final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel' + final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater' - await tester.longPressAt(textFieldStart + const Offset(50.0, 5.0)); + await tester.longPressAt(wPos); await tester.pump(const Duration(milliseconds: 50)); - await tester.tapAt(textFieldStart + const Offset(150.0, 5.0)); + await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor. - expect( - controller.selection, - const TextSelection.collapsed(offset: 8), - ); - await tester.tapAt(textFieldStart + const Offset(150.0, 5.0)); + expect(controller.selection.isCollapsed, isTrue); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9); + await tester.tapAt(pPos); await tester.pumpAndSettle(); // Double tap selection. @@ -2752,7 +2763,7 @@ void main() { const TextSelection(baseOffset: 8, extentOffset: 12), ); expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); - }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); + }, variant: const TargetPlatformVariant({ TargetPlatform.iOS })); testWidgets('force press selects word', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( @@ -2798,6 +2809,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -2808,15 +2822,15 @@ void main() { ), ); - final Offset textFieldStart = tester.getTopLeft(find.byType(CupertinoTextField)); + final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel' final int pointerValue = tester.nextPointer; final TestGesture gesture = await tester.createGesture(); await gesture.downWithCustomEvent( - textFieldStart + const Offset(150.0, 5.0), + pPos, PointerDownEvent( pointer: pointerValue, - position: textFieldStart + const Offset(150.0, 5.0), + position: pPos, // iPhone 6 and below report 0 across the board. pressure: 0, pressureMax: 0, @@ -2824,11 +2838,10 @@ void main() { ), ); await gesture.up(); - // Fall back to a single tap which selects the edge of the word. - expect( - controller.selection, - const TextSelection.collapsed(offset: 8), - ); + // Fall back to a single tap which selects the edge of the word on iOS, and + // a precise position on macOS. + expect(controller.selection.isCollapsed, isTrue); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9); await tester.pump(); // Falling back to a single tap doesn't trigger a toolbar. @@ -2839,6 +2852,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'abc def ghi', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( @@ -2860,7 +2876,7 @@ void main() { await tester.tapAt(ePos, pointer: 7); await tester.pump(const Duration(milliseconds: 50)); expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, 4); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 4 : 5); await tester.tapAt(ePos, pointer: 7); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 4); @@ -2897,6 +2913,103 @@ void main() { expect(controller.selection.extentOffset, 5); }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); + testWidgets('Selection updates on tap down (Desktop platforms)', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField(controller: controller), + ), + ), + ); + + const String testValue = 'abc def ghi'; + await tester.enterText(find.byType(CupertinoTextField), testValue); + // Skip past scrolling animation. + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + final Offset ePos = textOffsetToPosition(tester, 5); + final Offset gPos = textOffsetToPosition(tester, 8); + + final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); + await tester.pumpAndSettle(); + expect(controller.selection.baseOffset, 5); + expect(controller.selection.extentOffset, 5); + + await gesture.up(); + await tester.pumpAndSettle(kDoubleTapTimeout); + + await gesture.down(gPos); + await tester.pumpAndSettle(); + expect(controller.selection.baseOffset, 8); + expect(controller.selection.extentOffset, 8); + + // This should do nothing. The selection is set on tap down on desktop platforms. + await gesture.up(); + expect(controller.selection.baseOffset, 8); + expect(controller.selection.extentOffset, 8); + }, + variant: TargetPlatformVariant.desktop(), + ); + + testWidgets('Selection updates on tap up (Mobile platforms)', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS; + + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoTextField(controller: controller), + ), + ), + ); + + const String testValue = 'abc def ghi'; + await tester.enterText(find.byType(CupertinoTextField), testValue); + // Skip past scrolling animation. + await tester.pump(); + await tester.pump(const Duration(milliseconds: 200)); + + final Offset ePos = textOffsetToPosition(tester, 5); + final Offset gPos = textOffsetToPosition(tester, 8); + + final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); + await gesture.up(); + await tester.pumpAndSettle(kDoubleTapTimeout); + + await gesture.down(gPos); + await tester.pumpAndSettle(); + expect(controller.selection.baseOffset, 5); + expect(controller.selection.extentOffset, 5); + + await gesture.up(); + await tester.pumpAndSettle(kDoubleTapTimeout); + expect(controller.selection.baseOffset, 8); + expect(controller.selection.extentOffset, 8); + + final TestGesture touchGesture = await tester.startGesture(ePos); + await touchGesture.up(); + await tester.pumpAndSettle(kDoubleTapTimeout); + // On iOS, a tap to select, selects the word edge instead of the exact tap position. + expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5); + expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5); + + // Selection should stay the same since it is set on tap up for mobile platforms. + await touchGesture.down(gPos); + await tester.pumpAndSettle(); + expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5); + expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5); + + await touchGesture.up(); + await tester.pumpAndSettle(); + expect(controller.selection.baseOffset, 8); + expect(controller.selection.extentOffset, 8); + }, + variant: TargetPlatformVariant.mobile(), + ); + testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async { final TextEditingController controller = TextEditingController(); @@ -5191,6 +5304,7 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -5214,11 +5328,17 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); + if (isTargetPlatformMobile) { + await gesture.up(); + } await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 8); expect(controller.selection.extentOffset, 23); // Expand the selection a bit. + if (isTargetPlatformMobile) { + await gesture.down(textOffsetToPosition(tester, 24)); + } await gesture.moveTo(textOffsetToPosition(tester, 28)); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 8); @@ -5287,6 +5407,8 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android + || defaultTargetPlatform == TargetPlatform.fuchsia; await tester.pumpWidget( CupertinoApp( home: Center( @@ -5310,11 +5432,17 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); + if (isTargetPlatformMobile) { + await gesture.up(); + } await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 8); expect(controller.selection.extentOffset, 23); // Expand the selection a bit. + if (isTargetPlatformMobile) { + await gesture.down(textOffsetToPosition(tester, 24)); + } await gesture.moveTo(textOffsetToPosition(tester, 28)); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 8); @@ -5383,6 +5511,7 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( CupertinoApp( home: Center( @@ -5406,11 +5535,17 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); + if (isTargetPlatformMobile) { + await gesture.up(); + } await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 23); expect(controller.selection.extentOffset, 8); // Expand the selection a bit. + if (isTargetPlatformMobile) { + await gesture.down(textOffsetToPosition(tester, 7)); + } await gesture.moveTo(textOffsetToPosition(tester, 5)); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 23); @@ -5479,6 +5614,8 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android + || defaultTargetPlatform == TargetPlatform.fuchsia; await tester.pumpWidget( CupertinoApp( home: Center( @@ -5502,11 +5639,17 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); + if (isTargetPlatformMobile) { + await gesture.up(); + } await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 23); expect(controller.selection.extentOffset, 8); // Expand the selection a bit. + if (isTargetPlatformMobile) { + await gesture.down(textOffsetToPosition(tester, 7)); + } await gesture.moveTo(textOffsetToPosition(tester, 5)); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 23); diff --git a/packages/flutter/test/material/text_field_focus_test.dart b/packages/flutter/test/material/text_field_focus_test.dart index 813ce14a39cf..75cdd8ba8f83 100644 --- a/packages/flutter/test/material/text_field_focus_test.dart +++ b/packages/flutter/test/material/text_field_focus_test.dart @@ -475,8 +475,6 @@ void main() { final TestGesture down1 = await tester.startGesture(tester.getCenter(find.byType(TextField).first), kind: PointerDeviceKind.mouse); await tester.pump(); - await tester.pumpAndSettle(); - await down1.up(); await down1.removePointer(); expect(focusNodeA.hasFocus, true); diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 0b146ada68ad..971d266ffdf9 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -908,6 +908,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( MaterialApp( home: Material( @@ -933,7 +936,7 @@ void main() { // First tap moved the cursor. expect( controller.selection, - const TextSelection.collapsed(offset: 8), + TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9), ); await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); await tester.pump(); @@ -1084,12 +1087,12 @@ void main() { ), ); - final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + final Offset pos = textOffsetToPosition(tester, 9); // Index of 'P|eel' - await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); + await tester.tapAt(pos); await tester.pump(const Duration(milliseconds: 50)); - await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); + await tester.tapAt(pos); await tester.pump(); // Selected text shows 'Copy', and not 'Paste', 'Cut', 'Select all'. @@ -1901,6 +1904,99 @@ void main() { expect(controller.selection.baseOffset, testValue.length); }); + testWidgets('Selection updates on tap down (Desktop platforms)', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField(controller: controller), + ), + ), + ); + + const String testValue = 'abc def ghi'; + await tester.enterText(find.byType(TextField), testValue); + await skipPastScrollingAnimation(tester); + + final Offset ePos = textOffsetToPosition(tester, 5); + final Offset gPos = textOffsetToPosition(tester, 8); + + final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); + await tester.pumpAndSettle(); + expect(controller.selection.baseOffset, 5); + expect(controller.selection.extentOffset, 5); + + await gesture.up(); + await tester.pumpAndSettle(kDoubleTapTimeout); + + await gesture.down(gPos); + await tester.pumpAndSettle(); + expect(controller.selection.baseOffset, 8); + expect(controller.selection.extentOffset, 8); + + // This should do nothing. The selection is set on tap down on desktop platforms. + await gesture.up(); + expect(controller.selection.baseOffset, 8); + expect(controller.selection.extentOffset, 8); + }, + variant: TargetPlatformVariant.desktop(), + ); + + testWidgets('Selection updates on tap up (Mobile platforms)', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController(); + final bool isTargetPlatformApple = defaultTargetPlatform == TargetPlatform.iOS; + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField(controller: controller), + ), + ), + ); + + const String testValue = 'abc def ghi'; + await tester.enterText(find.byType(TextField), testValue); + await skipPastScrollingAnimation(tester); + + final Offset ePos = textOffsetToPosition(tester, 5); + final Offset gPos = textOffsetToPosition(tester, 8); + + final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse); + await gesture.up(); + await tester.pumpAndSettle(kDoubleTapTimeout); + + await gesture.down(gPos); + await tester.pumpAndSettle(); + expect(controller.selection.baseOffset, 5); + expect(controller.selection.extentOffset, 5); + + await gesture.up(); + await tester.pumpAndSettle(kDoubleTapTimeout); + expect(controller.selection.baseOffset, 8); + expect(controller.selection.extentOffset, 8); + + final TestGesture touchGesture = await tester.startGesture(ePos); + await touchGesture.up(); + await tester.pumpAndSettle(kDoubleTapTimeout); + // On iOS a tap to select, selects the word edge instead of the exact tap position. + expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5); + expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5); + + // Selection should stay the same since it is set on tap up for mobile platforms. + await touchGesture.down(gPos); + await tester.pumpAndSettle(); + expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5); + expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5); + + await touchGesture.up(); + await tester.pumpAndSettle(); + expect(controller.selection.baseOffset, 8); + expect(controller.selection.extentOffset, 8); + }, + variant: TargetPlatformVariant.mobile(), + ); + testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async { final TextEditingController controller = TextEditingController(); @@ -7110,7 +7206,7 @@ void main() { // But don't trigger the toolbar. expect(find.byType(CupertinoButton), findsNothing); }, - variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS }), + variant: const TargetPlatformVariant({ TargetPlatform.iOS }), ); testWidgets( @@ -7186,6 +7282,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( MaterialApp( home: Material( @@ -7198,18 +7297,16 @@ void main() { ), ); - final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + final Offset pos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r'. - await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); + await tester.tapAt(pos); await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); + await tester.tapAt(pos); await tester.pump(); // Plain collapsed selection. - expect( - controller.selection, - const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), - ); + expect(controller.selection.isCollapsed, isTrue); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6); // No toolbar. expect(find.byType(CupertinoButton), findsNothing); @@ -7223,6 +7320,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( MaterialApp( home: Material( @@ -7235,21 +7335,22 @@ void main() { ), ); - final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'. + final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater'. // This tap just puts the cursor somewhere different than where the double // tap will occur to test that the double tap moves the existing cursor first. - await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); + await tester.tapAt(wPos); await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); + await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor. expect( controller.selection, - const TextSelection.collapsed(offset: 8), + TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9), ); - await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); + await tester.tapAt(pPos); await tester.pumpAndSettle(); // Second tap selects the word around the cursor. @@ -7261,7 +7362,7 @@ void main() { // Selected text shows 3 toolbar buttons. expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); }, - variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS }), + variant: const TargetPlatformVariant({ TargetPlatform.iOS }), ); testWidgets( @@ -7679,6 +7780,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( MaterialApp( home: Material( @@ -7691,29 +7795,28 @@ void main() { ), ); - final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel'. + final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r' - await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); + await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor. expect( controller.selection, - const TextSelection.collapsed(offset: 8), + TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9), ); - await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); + await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(textfieldStart + const Offset(100.0, 9.0)); + await tester.tapAt(ePos); await tester.pump(); - // Plain collapsed selection at the edge of first word. In iOS 12, the - // first tap after a double tap ends up putting the cursor at where + // Plain collapsed selection at the edge of first word on iOS. In iOS 12, + // the first tap after a double tap ends up putting the cursor at where // you tapped instead of the edge like every other single tap. This is // likely a bug in iOS 12 and not present in other versions. - expect( - controller.selection, - const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), - ); + expect(controller.selection.isCollapsed, isTrue); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6); // No toolbar. expect(find.byType(CupertinoButton), findsNothing); @@ -7796,6 +7899,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( MaterialApp( home: Material( @@ -7808,20 +7914,18 @@ void main() { ), ); - final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r' - await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0)); + await tester.longPressAt(ePos); await tester.pump(const Duration(milliseconds: 50)); - await tester.tapAt(textfieldStart + const Offset(50.0, 9.0)); + await tester.tapAt(ePos); await tester.pump(); // We ended up moving the cursor to the edge of the same word and dismissed // the toolbar. - expect( - controller.selection, - const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), - ); + expect(controller.selection.isCollapsed, isTrue); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 6); // Collapsed toolbar shows 2 buttons. expect(find.byType(CupertinoButton), findsNothing); @@ -8358,6 +8462,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( MaterialApp( home: Material( @@ -8370,19 +8477,20 @@ void main() { ), ); - final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel' + final Offset ePos = textOffsetToPosition(tester, 6); // Index of 'Atwate|r' - await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); + await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor to the beginning of the second word. expect( controller.selection, - const TextSelection.collapsed(offset: 8), + TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9), ); - await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); + await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 500)); - await tester.longPressAt(textfieldStart + const Offset(100.0, 9.0)); + await tester.longPressAt(ePos); await tester.pumpAndSettle(); // Plain collapsed selection at the exact tap position. @@ -8403,6 +8511,9 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + // On iOS/iPadOS, during a tap we select the edge of the word closest to the tap. + // On macOS, we select the precise position of the tap. + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( MaterialApp( home: Material( @@ -8415,23 +8526,24 @@ void main() { ), ); - final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); + final Offset pPos = textOffsetToPosition(tester, 9); // Index of 'P|eel' + final Offset wPos = textOffsetToPosition(tester, 3); // Index of 'Atw|ater' - await tester.longPressAt(textfieldStart + const Offset(50.0, 9.0)); + await tester.longPressAt(wPos); await tester.pump(const Duration(milliseconds: 50)); expect( controller.selection, const TextSelection.collapsed(offset: 3), ); - await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); + await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor. expect( controller.selection, - const TextSelection.collapsed(offset: 8), + TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9), ); - await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); + await tester.tapAt(pPos); await tester.pumpAndSettle(); // Double tap selection. @@ -8568,7 +8680,7 @@ void main() { ); expect(find.byType(CupertinoButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(3)); }, - variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS }), + variant: const TargetPlatformVariant({ TargetPlatform.iOS }), ); testWidgets( @@ -10959,6 +11071,7 @@ void main() { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( MaterialApp( home: Material( @@ -10982,11 +11095,18 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); + if (isTargetPlatformMobile) { + await gesture.up(); + } await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 8); expect(controller.selection.extentOffset, 23); // Expand the selection a bit. + if (isTargetPlatformMobile) { + await gesture.down(textOffsetToPosition(tester, 24)); + } + await tester.pumpAndSettle(); await gesture.moveTo(textOffsetToPosition(tester, 28)); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 8); @@ -11049,12 +11169,14 @@ void main() { await gesture.up(); expect(controller.selection.baseOffset, 8); expect(controller.selection.extentOffset, 26); - }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); + }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); testWidgets('can shift + tap + drag to select with a keyboard (non-Apple platforms)', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android + || defaultTargetPlatform == TargetPlatform.fuchsia; await tester.pumpWidget( MaterialApp( home: Material( @@ -11078,11 +11200,17 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); + if (isTargetPlatformMobile) { + await gesture.up(); + } await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 8); expect(controller.selection.extentOffset, 23); // Expand the selection a bit. + if (isTargetPlatformMobile) { + await gesture.down(textOffsetToPosition(tester, 23)); + } await gesture.moveTo(textOffsetToPosition(tester, 28)); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 8); @@ -11145,12 +11273,13 @@ void main() { await gesture.up(); expect(controller.selection.baseOffset, 8); expect(controller.selection.extentOffset, 26); - }, variant: const TargetPlatformVariant({ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows })); + }, variant: const TargetPlatformVariant({ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows })); testWidgets('can shift + tap + drag to select with a keyboard, reversed (Apple platforms)', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.iOS; await tester.pumpWidget( MaterialApp( home: Material( @@ -11174,11 +11303,17 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); + if (isTargetPlatformMobile) { + await gesture.up(); + } await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 23); expect(controller.selection.extentOffset, 8); // Expand the selection a bit. + if (isTargetPlatformMobile) { + await gesture.down(textOffsetToPosition(tester, 7)); + } await gesture.moveTo(textOffsetToPosition(tester, 5)); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 23); @@ -11241,12 +11376,14 @@ void main() { await gesture.up(); expect(controller.selection.baseOffset, 23); expect(controller.selection.extentOffset, 14); - }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); + }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); testWidgets('can shift + tap + drag to select with a keyboard, reversed (non-Apple platforms)', (WidgetTester tester) async { final TextEditingController controller = TextEditingController( text: 'Atwater Peel Sherbrooke Bonaventure', ); + final bool isTargetPlatformMobile = defaultTargetPlatform == TargetPlatform.android + || defaultTargetPlatform == TargetPlatform.fuchsia; await tester.pumpWidget( MaterialApp( home: Material( @@ -11270,11 +11407,17 @@ void main() { pointer: 7, kind: PointerDeviceKind.mouse, ); + if (isTargetPlatformMobile) { + await gesture.up(); + } await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 23); expect(controller.selection.extentOffset, 8); // Expand the selection a bit. + if (isTargetPlatformMobile) { + await gesture.down(textOffsetToPosition(tester, 8)); + } await gesture.moveTo(textOffsetToPosition(tester, 5)); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 23); @@ -11337,7 +11480,7 @@ void main() { await gesture.up(); expect(controller.selection.baseOffset, 23); expect(controller.selection.extentOffset, 14); - }, variant: const TargetPlatformVariant({ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows })); + }, variant: const TargetPlatformVariant({ TargetPlatform.linux, TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.windows })); // Regression test for https://github.com/flutter/flutter/issues/101587. testWidgets('Right clicking menu behavior', (WidgetTester tester) async { diff --git a/packages/flutter/test/widgets/text_selection_test.dart b/packages/flutter/test/widgets/text_selection_test.dart index fba6e2532613..09980cd96268 100644 --- a/packages/flutter/test/widgets/text_selection_test.dart +++ b/packages/flutter/test/widgets/text_selection_test.dart @@ -557,9 +557,9 @@ void main() { switch (defaultTargetPlatform) { case TargetPlatform.iOS: - case TargetPlatform.macOS: expect(renderEditable.selectWordEdgeCalled, isTrue); break; + case TargetPlatform.macOS: case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.linux: