Makes text selection match the native behavior (#79308)
diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart
index 4026111..8d4f26d 100644
--- a/packages/flutter/lib/src/rendering/editable.dart
+++ b/packages/flutter/lib/src/rendering/editable.dart
@@ -3042,22 +3042,43 @@
// If text is obscured, the entire sentence should be treated as one word.
if (obscureText) {
return TextSelection(baseOffset: 0, extentOffset: _plainText.length);
- // If the word is a space, on iOS try to select the previous word instead.
- // On Android try to select the previous word instead only if the text is read only.
+ // On iOS, select the previous word if there is a previous word, or select
+ // to the end of the next word if there is a next word. Select nothing if
+ // there is neither a previous word nor a next word.
+ //
+ // If the platform is Android and the text is read only, try to select the
+ // previous word if there is one; otherwise, select the single whitespace at
+ // the position.
} else if (_isWhitespace(_plainText.codeUnitAt(position.offset))
&& position.offset > 0) {
assert(defaultTargetPlatform != null);
final TextRange? previousWord = _getPreviousWord(word.start);
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
+ if (previousWord == null) {
+ final TextRange? nextWord = _getNextWord(word.start);
+ if (nextWord == null) {
+ return TextSelection.collapsed(offset: position.offset);
+ }
+ return TextSelection(
+ baseOffset: position.offset,
+ extentOffset: nextWord.end,
+ );
+ }
return TextSelection(
- baseOffset: previousWord!.start,
+ baseOffset: previousWord.start,
extentOffset: position.offset,
);
case TargetPlatform.android:
if (readOnly) {
+ if (previousWord == null) {
+ return TextSelection(
+ baseOffset: position.offset,
+ extentOffset: position.offset + 1,
+ );
+ }
return TextSelection(
- baseOffset: previousWord!.start,
+ baseOffset: previousWord.start,
extentOffset: position.offset,
);
}
diff --git a/packages/flutter/test/rendering/editable_test.dart b/packages/flutter/test/rendering/editable_test.dart
index 1081703..afc0c8d 100644
--- a/packages/flutter/test/rendering/editable_test.dart
+++ b/packages/flutter/test/rendering/editable_test.dart
@@ -499,6 +499,136 @@
expect(currentSelection.extentOffset, 9);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61026
+ test('selects readonly renderEditable matches native behavior for android', () {
+ // Regression test for https://github.com/flutter/flutter/issues/79166.
+ final TargetPlatform? previousPlatform = debugDefaultTargetPlatformOverride;
+ debugDefaultTargetPlatformOverride = TargetPlatform.android;
+ const String text = ' test';
+ final TextSelectionDelegate delegate = FakeEditableTextState()
+ ..textEditingValue = const TextEditingValue(text: text);
+ final ViewportOffset viewportOffset = ViewportOffset.zero();
+ late TextSelection currentSelection;
+ final RenderEditable editable = RenderEditable(
+ backgroundCursorColor: Colors.grey,
+ selectionColor: Colors.black,
+ textDirection: TextDirection.ltr,
+ cursorColor: Colors.red,
+ readOnly: true,
+ offset: viewportOffset,
+ textSelectionDelegate: delegate,
+ onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
+ currentSelection = selection;
+ },
+ startHandleLayerLink: LayerLink(),
+ endHandleLayerLink: LayerLink(),
+ text: const TextSpan(
+ text: text,
+ style: TextStyle(
+ height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
+ ),
+ ),
+ selection: const TextSelection.collapsed(
+ offset: 4,
+ ),
+ );
+
+ layout(editable);
+
+ // Select the second white space, where the text position = 1.
+ editable.selectWordsInRange(from: const Offset(10, 2), cause:SelectionChangedCause.longPress);
+ pumpFrame();
+ expect(currentSelection.isCollapsed, false);
+ expect(currentSelection.baseOffset, 1);
+ expect(currentSelection.extentOffset, 2);
+ debugDefaultTargetPlatformOverride = previousPlatform;
+ });
+
+ test('selects renderEditable matches native behavior for iOS case 1', () {
+ // Regression test for https://github.com/flutter/flutter/issues/79166.
+ final TargetPlatform? previousPlatform = debugDefaultTargetPlatformOverride;
+ debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
+ const String text = ' test';
+ final TextSelectionDelegate delegate = FakeEditableTextState()
+ ..textEditingValue = const TextEditingValue(text: text);
+ final ViewportOffset viewportOffset = ViewportOffset.zero();
+ late TextSelection currentSelection;
+ final RenderEditable editable = RenderEditable(
+ backgroundCursorColor: Colors.grey,
+ selectionColor: Colors.black,
+ textDirection: TextDirection.ltr,
+ cursorColor: Colors.red,
+ offset: viewportOffset,
+ textSelectionDelegate: delegate,
+ onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
+ currentSelection = selection;
+ },
+ startHandleLayerLink: LayerLink(),
+ endHandleLayerLink: LayerLink(),
+ text: const TextSpan(
+ text: text,
+ style: TextStyle(
+ height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
+ ),
+ ),
+ selection: const TextSelection.collapsed(
+ offset: 4,
+ ),
+ );
+
+ layout(editable);
+
+ // Select the second white space, where the text position = 1.
+ editable.selectWordsInRange(from: const Offset(10, 2), cause:SelectionChangedCause.longPress);
+ pumpFrame();
+ expect(currentSelection.isCollapsed, false);
+ expect(currentSelection.baseOffset, 1);
+ expect(currentSelection.extentOffset, 6);
+ debugDefaultTargetPlatformOverride = previousPlatform;
+ });
+
+ test('selects renderEditable matches native behavior for iOS case 2', () {
+ // Regression test for https://github.com/flutter/flutter/issues/79166.
+ final TargetPlatform? previousPlatform = debugDefaultTargetPlatformOverride;
+ debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
+ const String text = ' ';
+ final TextSelectionDelegate delegate = FakeEditableTextState()
+ ..textEditingValue = const TextEditingValue(text: text);
+ final ViewportOffset viewportOffset = ViewportOffset.zero();
+ late TextSelection currentSelection;
+ final RenderEditable editable = RenderEditable(
+ backgroundCursorColor: Colors.grey,
+ selectionColor: Colors.black,
+ textDirection: TextDirection.ltr,
+ cursorColor: Colors.red,
+ offset: viewportOffset,
+ textSelectionDelegate: delegate,
+ onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
+ currentSelection = selection;
+ },
+ startHandleLayerLink: LayerLink(),
+ endHandleLayerLink: LayerLink(),
+ text: const TextSpan(
+ text: text,
+ style: TextStyle(
+ height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
+ ),
+ ),
+ selection: const TextSelection.collapsed(
+ offset: 4,
+ ),
+ );
+
+ layout(editable);
+
+ // Select the second white space, where the text position = 1.
+ editable.selectWordsInRange(from: const Offset(10, 2), cause:SelectionChangedCause.longPress);
+ pumpFrame();
+ expect(currentSelection.isCollapsed, true);
+ expect(currentSelection.baseOffset, 1);
+ expect(currentSelection.extentOffset, 1);
+ debugDefaultTargetPlatformOverride = previousPlatform;
+ });
+
test('selects correct place when offsets are flipped', () {
const String text = 'abc def ghi';
final TextSelectionDelegate delegate = FakeEditableTextState()