diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index 93429ca864d8..b5a3b59ea14d 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -2149,7 +2149,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition! - _paintOffset)); final TextRange word = _textPainter.getWordBoundary(position); late TextSelection newSelection; - if (position.offset - word.start <= 1) { + if (position.offset <= word.start) { newSelection = TextSelection.collapsed(offset: word.start); } else { newSelection = TextSelection.collapsed(offset: word.end, affinity: TextAffinity.upstream); diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index aa612ff52dc5..e0ad8ee868ee 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -2173,7 +2173,7 @@ void main() { await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor. expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9); await tester.tapAt(pPos); await tester.pumpAndSettle(); @@ -2275,7 +2275,7 @@ void main() { await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor. expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9); await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 500)); @@ -3134,7 +3134,7 @@ void main() { await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor to the beginning of the second word. expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9); await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 500)); @@ -3201,7 +3201,7 @@ void main() { // First tap moved the cursor. expect(find.byType(CupertinoButton), findsNothing); expect(controller.selection.isCollapsed, isTrue); - expect(controller.selection.baseOffset, isTargetPlatformMobile ? 8 : 9); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9); await tester.tapAt(pPos); await tester.pumpAndSettle(); @@ -3270,7 +3270,7 @@ void main() { // First tap moved the cursor and hides the toolbar. expect( controller.selection, - const TextSelection.collapsed(offset: 8), + const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream), ); expect(find.byType(CupertinoButton), findsNothing); await tester.tapAt(textFieldStart + const Offset(150.0, 5.0)); @@ -3375,7 +3375,7 @@ void main() { // 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); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 12 : 9); await tester.pump(); // Falling back to a single tap doesn't trigger a toolbar. @@ -3410,7 +3410,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, isTargetPlatformMobile ? 4 : 5); + expect(controller.selection.baseOffset, isTargetPlatformMobile ? 7 : 5); await tester.tapAt(ePos, pointer: 7); await tester.pumpAndSettle(); expect(controller.selection.baseOffset, 4); @@ -3911,14 +3911,14 @@ void main() { 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); + expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5); + expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5); // Selection should stay the same since it is set on tap up for mobile platforms. await touchGesture.down(gPos); await tester.pump(); - expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5); - expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5); + expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5); + expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5); await touchGesture.up(); await tester.pumpAndSettle(); @@ -7219,7 +7219,7 @@ void main() { await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); await tester.pumpAndSettle(const Duration(milliseconds: 300)); expect(controller.selection.isCollapsed, true); - expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 4); + expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 7); expect(find.byKey(fakeMagnifier.key!), findsNothing); // Long press the 'e' to move the cursor in front of the 'e' and show the magnifier. diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 7693bec5e603..944e444d1186 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -1017,7 +1017,6 @@ void main() { ); // 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( @@ -1031,21 +1030,19 @@ void main() { ), ); - final Offset textfieldStart = tester.getTopLeft(find.byType(TextField)); - // 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(textOffsetToPosition(tester, 3)); await tester.pump(const Duration(milliseconds: 500)); - await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); + await tester.tapAt(textOffsetToPosition(tester, 8)); await tester.pump(const Duration(milliseconds: 50)); // First tap moved the cursor. expect( controller.selection, - TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9), + const TextSelection.collapsed(offset: 8), ); - await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); + await tester.tapAt(textOffsetToPosition(tester, 8)); await tester.pump(); // Second tap selects the word around the cursor. @@ -2088,14 +2085,14 @@ void main() { 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); + expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5); + expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5); // Selection should stay the same since it is set on tap up for mobile platforms. await touchGesture.down(gPos); await tester.pump(); - expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5); - expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5); + expect(controller.selection.baseOffset, isTargetPlatformApple ? 7 : 5); + expect(controller.selection.extentOffset, isTargetPlatformApple ? 7 : 5); await touchGesture.up(); await tester.pumpAndSettle(); @@ -8414,14 +8411,11 @@ void main() { ); testWidgets( - 'double tap selects word and first tap of double tap moves cursor', + 'double tap selects word and first tap of double tap moves cursor (iOS)', (WidgetTester tester) async { 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( @@ -8447,7 +8441,7 @@ void main() { // First tap moved the cursor. expect( controller.selection, - TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9), + const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream), ); await tester.tapAt(pPos); await tester.pumpAndSettle(); @@ -8464,6 +8458,37 @@ void main() { variant: const TargetPlatformVariant({ TargetPlatform.iOS }), ); + testWidgets('iOS selectWordEdge works correctly', (WidgetTester tester) async { + final TextEditingController controller = TextEditingController( + text: 'blah1 blah2', + ); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TextField( + controller: controller, + ), + ), + ), + ); + + // Initially, the menu is not shown and there is no selection. + expect(controller.selection, const TextSelection(baseOffset: -1, extentOffset: -1)); + final Offset pos1 = textOffsetToPosition(tester, 1); + TestGesture gesture = await tester.startGesture(pos1); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection.collapsed(offset: 5, affinity: TextAffinity.upstream)); + + final Offset pos0 = textOffsetToPosition(tester, 0); + gesture = await tester.startGesture(pos0); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + expect(controller.selection, const TextSelection.collapsed(offset: 0)); + }, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); + testWidgets( 'double tap does not select word on read-only obscured field', (WidgetTester tester) async { @@ -8952,7 +8977,7 @@ void main() { // First tap moved the cursor. expect( controller.selection, - TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9), + isTargetPlatformMobile ? const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream) : const TextSelection.collapsed(offset: 9), ); await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 500)); @@ -9813,7 +9838,7 @@ void main() { // First tap moved the cursor to the beginning of the second word. expect( controller.selection, - TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9), + isTargetPlatformMobile ? const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream) : const TextSelection.collapsed(offset: 9), ); await tester.tapAt(pPos); await tester.pump(const Duration(milliseconds: 500)); @@ -9875,7 +9900,7 @@ void main() { // First tap moved the cursor. expect( controller.selection, - TextSelection.collapsed(offset: isTargetPlatformMobile ? 8 : 9), + isTargetPlatformMobile ? const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream) : const TextSelection.collapsed(offset: 9), ); await tester.tapAt(pPos); await tester.pumpAndSettle(); @@ -10006,7 +10031,7 @@ void main() { // First tap moved the cursor and hid the toolbar. expect( controller.selection, - const TextSelection.collapsed(offset: 8), + const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream) ); expect(find.byType(CupertinoButton), findsNothing); await tester.tapAt(textfieldStart + const Offset(150.0, 9.0)); @@ -10441,7 +10466,7 @@ void main() { // Single taps selects the edge of the word. expect( controller.selection, - const TextSelection.collapsed(offset: 8), + const TextSelection.collapsed(offset: 12, affinity: TextAffinity.upstream), ); await tester.pump(); @@ -13418,7 +13443,7 @@ void main() { await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e'))); await tester.pumpAndSettle(const Duration(milliseconds: 300)); expect(controller.selection.isCollapsed, true); - expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 4); + expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 7); expect(find.byKey(fakeMagnifier.key!), findsNothing); // Long press the 'e' to select 'def' on Android and show magnifier. diff --git a/packages/flutter/test/widgets/selectable_text_test.dart b/packages/flutter/test/widgets/selectable_text_test.dart index 3b7aef895e32..8ffefd2bc1d6 100644 --- a/packages/flutter/test/widgets/selectable_text_test.dart +++ b/packages/flutter/test/widgets/selectable_text_test.dart @@ -3885,7 +3885,6 @@ void main() { ), ), ); - final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText)); await tester.tapAt(selectableTextStart + const Offset(50.0, 5.0)); @@ -3912,7 +3911,7 @@ void main() { // First tap moved the cursor. expect( controller.selection, - const TextSelection.collapsed(offset: 0), + const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream), ); await tester.tapAt(selectableTextStart + const Offset(10.0, 5.0)); await tester.pump(const Duration(milliseconds: 50));