Makes text selection match the native behavior (#79308)
This commit is contained in:
@@ -3042,22 +3042,43 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -499,6 +499,136 @@ void main() {
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user