Skip to content

Commit 29397c2

Browse files
Fix selectWordsInRange when last word is located before the first word (#113224)
1 parent bd9021a commit 29397c2

File tree

3 files changed

+195
-23
lines changed

3 files changed

+195
-23
lines changed

packages/flutter/lib/src/rendering/editable.dart

+12-8
Original file line numberDiff line numberDiff line change
@@ -2066,7 +2066,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
20662066
selectWordsInRange(from: _lastTapDownPosition!, cause: cause);
20672067
}
20682068

2069-
/// Selects the set words of a paragraph in a given range of global positions.
2069+
/// Selects the set words of a paragraph that intersect a given range of global positions.
2070+
///
2071+
/// The set of words selected are not strictly bounded by the range of global positions.
20702072
///
20712073
/// The first and last endpoints of the selection will always be at the
20722074
/// beginning and end of a word respectively.
@@ -2076,15 +2078,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
20762078
assert(cause != null);
20772079
assert(from != null);
20782080
_computeTextMetricsIfNeeded();
2079-
final TextPosition firstPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
2080-
final TextSelection firstWord = _getWordAtOffset(firstPosition);
2081-
final TextSelection lastWord = to == null ?
2082-
firstWord : _getWordAtOffset(_textPainter.getPositionForOffset(globalToLocal(to - _paintOffset)));
2081+
final TextPosition fromPosition = _textPainter.getPositionForOffset(globalToLocal(from - _paintOffset));
2082+
final TextSelection fromWord = _getWordAtOffset(fromPosition);
2083+
final TextPosition toPosition = to == null ? fromPosition : _textPainter.getPositionForOffset(globalToLocal(to - _paintOffset));
2084+
final TextSelection toWord = toPosition == fromPosition ? fromWord : _getWordAtOffset(toPosition);
2085+
final bool isFromWordBeforeToWord = fromWord.start < toWord.end;
2086+
20832087
_setSelection(
20842088
TextSelection(
2085-
baseOffset: firstWord.base.offset,
2086-
extentOffset: lastWord.extent.offset,
2087-
affinity: firstWord.affinity,
2089+
baseOffset: isFromWordBeforeToWord ? fromWord.base.offset : fromWord.extent.offset,
2090+
extentOffset: isFromWordBeforeToWord ? toWord.extent.offset : toWord.base.offset,
2091+
affinity: fromWord.affinity,
20882092
),
20892093
cause,
20902094
);

packages/flutter/test/material/text_field_test.dart

+78
Original file line numberDiff line numberDiff line change
@@ -8432,6 +8432,84 @@ void main() {
84328432
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
84338433
);
84348434

8435+
testWidgets(
8436+
'long press drag extends the selection to the word under the drag and shows toolbar on lift on non-Apple platforms',
8437+
(WidgetTester tester) async {
8438+
final TextEditingController controller = TextEditingController(
8439+
text: 'Atwater Peel Sherbrooke Bonaventure',
8440+
);
8441+
await tester.pumpWidget(
8442+
MaterialApp(
8443+
home: Material(
8444+
child: Center(
8445+
child: TextField(
8446+
controller: controller,
8447+
),
8448+
),
8449+
),
8450+
),
8451+
);
8452+
8453+
final TestGesture gesture =
8454+
await tester.startGesture(textOffsetToPosition(tester, 18));
8455+
await tester.pump(const Duration(milliseconds: 500));
8456+
8457+
// Long press selects the word at the long presses position.
8458+
expect(
8459+
controller.selection,
8460+
const TextSelection(baseOffset: 13, extentOffset: 23),
8461+
);
8462+
// Cursor move doesn't trigger a toolbar initially.
8463+
expect(find.byType(TextButton), findsNothing);
8464+
8465+
await gesture.moveBy(const Offset(100, 0));
8466+
await tester.pump();
8467+
8468+
// The selection is now moved with the drag.
8469+
expect(
8470+
controller.selection,
8471+
const TextSelection(baseOffset: 13, extentOffset: 35),
8472+
);
8473+
// Still no toolbar.
8474+
expect(find.byType(TextButton), findsNothing);
8475+
8476+
// The selection is moved on a backwards drag.
8477+
await gesture.moveBy(const Offset(-200, 0));
8478+
await tester.pump();
8479+
8480+
// The selection is now moved with the drag.
8481+
expect(
8482+
controller.selection,
8483+
const TextSelection(baseOffset: 23, extentOffset: 8),
8484+
);
8485+
// Still no toolbar.
8486+
expect(find.byType(TextButton), findsNothing);
8487+
8488+
await gesture.moveBy(const Offset(-100, 0));
8489+
await tester.pump();
8490+
8491+
// The selection is now moved with the drag.
8492+
expect(
8493+
controller.selection,
8494+
const TextSelection(baseOffset: 23, extentOffset: 0),
8495+
);
8496+
// Still no toolbar.
8497+
expect(find.byType(TextButton), findsNothing);
8498+
8499+
await gesture.up();
8500+
await tester.pumpAndSettle();
8501+
8502+
// The selection isn't affected by the gesture lift.
8503+
expect(
8504+
controller.selection,
8505+
const TextSelection(baseOffset: 23, extentOffset: 0),
8506+
);
8507+
// The toolbar now shows up.
8508+
expect(find.byType(TextButton), isContextMenuProvidedByPlatform ? findsNothing : findsNWidgets(4));
8509+
},
8510+
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
8511+
);
8512+
84358513
testWidgets(
84368514
'long press drag moves the cursor under the drag and shows toolbar on lift',
84378515
(WidgetTester tester) async {

packages/flutter/test/widgets/selectable_text_test.dart

+105-15
Original file line numberDiff line numberDiff line change
@@ -3372,7 +3372,7 @@ void main() {
33723372
);
33733373

33743374
testWidgets(
3375-
'long press drag moves the cursor under the drag and shows toolbar on lift (iOS)',
3375+
'long press drag extends the selection to the word under the drag and shows toolbar on lift on non-Apple platforms',
33763376
(WidgetTester tester) async {
33773377
await tester.pumpWidget(
33783378
const MaterialApp(
@@ -3384,10 +3384,84 @@ void main() {
33843384
),
33853385
);
33863386

3387-
final Offset selectableTextStart = tester.getTopLeft(find.byType(SelectableText));
3387+
final TestGesture gesture =
3388+
await tester.startGesture(textOffsetToPosition(tester, 18));
3389+
await tester.pump(const Duration(milliseconds: 500));
3390+
3391+
final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
3392+
final TextEditingController controller = editableTextWidget.controller;
3393+
3394+
// Long press selects the word at the long presses position.
3395+
expect(
3396+
controller.selection,
3397+
const TextSelection(baseOffset: 13, extentOffset: 23),
3398+
);
3399+
// Cursor move doesn't trigger a toolbar initially.
3400+
expect(find.byType(TextButton), findsNothing);
3401+
3402+
await gesture.moveBy(const Offset(100, 0));
3403+
await tester.pump();
3404+
3405+
// The selection is now moved with the drag.
3406+
expect(
3407+
controller.selection,
3408+
const TextSelection(baseOffset: 13, extentOffset: 35),
3409+
);
3410+
// Still no toolbar.
3411+
expect(find.byType(TextButton), findsNothing);
3412+
3413+
// The selection is moved on a backwards drag.
3414+
await gesture.moveBy(const Offset(-200, 0));
3415+
await tester.pump();
3416+
3417+
// The selection is now moved with the drag.
3418+
expect(
3419+
controller.selection,
3420+
const TextSelection(baseOffset: 23, extentOffset: 8),
3421+
);
3422+
// Still no toolbar.
3423+
expect(find.byType(TextButton), findsNothing);
3424+
3425+
await gesture.moveBy(const Offset(-100, 0));
3426+
await tester.pump();
3427+
3428+
// The selection is now moved with the drag.
3429+
expect(
3430+
controller.selection,
3431+
const TextSelection(baseOffset: 23, extentOffset: 0),
3432+
);
3433+
// Still no toolbar.
3434+
expect(find.byType(TextButton), findsNothing);
3435+
3436+
await gesture.up();
3437+
await tester.pumpAndSettle();
3438+
3439+
// The selection isn't affected by the gesture lift.
3440+
expect(
3441+
controller.selection,
3442+
const TextSelection(baseOffset: 23, extentOffset: 0),
3443+
);
3444+
// The toolbar now shows up.
3445+
expect(find.byType(TextButton), findsNWidgets(2));
3446+
},
3447+
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
3448+
);
3449+
3450+
testWidgets(
3451+
'long press drag extends the selection to the word under the drag and shows toolbar on lift (iOS)',
3452+
(WidgetTester tester) async {
3453+
await tester.pumpWidget(
3454+
const MaterialApp(
3455+
home: Material(
3456+
child: Center(
3457+
child: SelectableText('Atwater Peel Sherbrooke Bonaventure'),
3458+
),
3459+
),
3460+
),
3461+
);
33883462

33893463
final TestGesture gesture =
3390-
await tester.startGesture(selectableTextStart + const Offset(50.0, 5.0));
3464+
await tester.startGesture(textOffsetToPosition(tester, 18));
33913465
await tester.pump(const Duration(milliseconds: 500));
33923466

33933467
final EditableText editableTextWidget = tester.widget(find.byType(EditableText).first);
@@ -3397,36 +3471,52 @@ void main() {
33973471
expect(
33983472
controller.selection,
33993473
const TextSelection(
3400-
baseOffset: 0,
3401-
extentOffset: 7,
3474+
baseOffset: 13,
3475+
extentOffset: 23,
34023476
),
34033477
);
3404-
// Cursor move doesn't trigger a toolbar initially.
3478+
// Word select doesn't trigger a toolbar initially.
34053479
expect(find.byType(CupertinoButton), findsNothing);
34063480

34073481
await gesture.moveBy(const Offset(100, 0));
34083482
await tester.pump();
34093483

3410-
// The selection position is now moved with the drag.
3484+
// The selection is now moved with the drag.
34113485
expect(
34123486
controller.selection,
34133487
const TextSelection(
3414-
baseOffset: 0,
3415-
extentOffset: 12,
3488+
baseOffset: 13,
3489+
extentOffset: 35,
34163490
),
34173491
);
34183492
// Still no toolbar.
34193493
expect(find.byType(CupertinoButton), findsNothing);
34203494

3421-
await gesture.moveBy(const Offset(100, 0));
3495+
// The selection is moved with a backwards drag.
3496+
await gesture.moveBy(const Offset(-200, 0));
34223497
await tester.pump();
34233498

3424-
// The selection position is now moved with the drag.
3499+
// The selection is now moved with the drag.
34253500
expect(
34263501
controller.selection,
34273502
const TextSelection(
3428-
baseOffset: 0,
3429-
extentOffset: 23,
3503+
baseOffset: 23,
3504+
extentOffset: 8,
3505+
),
3506+
);
3507+
// Still no toolbar.
3508+
expect(find.byType(CupertinoButton), findsNothing);
3509+
3510+
// The selection is moved with a backwards drag.
3511+
await gesture.moveBy(const Offset(-100, 0));
3512+
await tester.pump();
3513+
3514+
// The selection is now moved with the drag.
3515+
expect(
3516+
controller.selection,
3517+
const TextSelection(
3518+
baseOffset: 23,
3519+
extentOffset: 0,
34303520
),
34313521
);
34323522
// Still no toolbar.
@@ -3439,8 +3529,8 @@ void main() {
34393529
expect(
34403530
controller.selection,
34413531
const TextSelection(
3442-
baseOffset: 0,
3443-
extentOffset: 23,
3532+
baseOffset: 23,
3533+
extentOffset: 0,
34443534
),
34453535
);
34463536
// The toolbar now shows up.

0 commit comments

Comments
 (0)