Unverified Commit 4455e86d authored by Chris Bracken's avatar Chris Bracken Committed by GitHub

Send caret rect to embedder on selection update (#137863)

Background: In the framework, the position of the caret rect is updated on each cursor position change such that if the user initiates composing input, the current cursor position can be used for the first character until the composing rect can be sent.

Previously, no update was sent on selection changes, on the assumption that the most recent cursor position will remain the correct position for the duration of the selection. While this is the case for forward selections, it is an incorrect assumption for reversed selections, where selection.base > selection.extent.

We now update the cursor position during selection changes such that the cursor position sent to the embedder is always the position at which next text input would occur. This is the start position of the selection or min(selection.baseOffset, selection.extentOffset).

Issue: https://github.com/flutter/flutter/issues/137677
parent 71db445c
...@@ -4115,11 +4115,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -4115,11 +4115,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_textInputConnection!.setSelectionRects(rects); _textInputConnection!.setSelectionRects(rects);
} }
// Sends the current composing rect to the iOS text input plugin via the text // Sends the current composing rect to the embedder's text input plugin.
// input channel. We need to keep sending the information even if no text is //
// currently marked, as the information usually lags behind. The text input // In cases where the composing rect hasn't been updated in the embedder due
// plugin needs to estimate the composing rect based on the latest caret rect, // to the lag of asynchronous messages over the channel, the position of the
// when the composing rect info didn't arrive in time. // current caret rect is used instead.
//
// See: [_updateCaretRectIfNeeded]
void _updateComposingRectIfNeeded() { void _updateComposingRectIfNeeded() {
final TextRange composingRange = _value.composing; final TextRange composingRange = _value.composing;
assert(mounted); assert(mounted);
...@@ -4133,12 +4135,24 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -4133,12 +4135,24 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_textInputConnection!.setComposingRect(composingRect); _textInputConnection!.setComposingRect(composingRect);
} }
// Sends the current caret rect to the embedder's text input plugin.
//
// The position of the caret rect is updated periodically such that if the
// user initiates composing input, the current cursor rect can be used for
// the first character until the composing rect can be sent.
//
// On selection changes, the start of the selection is used. This ensures
// that regardless of the direction the selection was created, the cursor is
// set to the position where next text input occurs. This position is used to
// position the IME's candidate selection menu.
//
// See: [_updateComposingRectIfNeeded]
void _updateCaretRectIfNeeded() { void _updateCaretRectIfNeeded() {
final TextSelection? selection = renderEditable.selection; final TextSelection? selection = renderEditable.selection;
if (selection == null || !selection.isValid || !selection.isCollapsed) { if (selection == null || !selection.isValid) {
return; return;
} }
final TextPosition currentTextPosition = TextPosition(offset: selection.baseOffset); final TextPosition currentTextPosition = TextPosition(offset: selection.start);
final Rect caretRect = renderEditable.getLocalRectForCaret(currentTextPosition); final Rect caretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
_textInputConnection!.setCaretRect(caretRect); _textInputConnection!.setCaretRect(caretRect);
} }
......
...@@ -5875,16 +5875,48 @@ void main() { ...@@ -5875,16 +5875,48 @@ void main() {
); );
testWidgetsWithLeakTracking( testWidgetsWithLeakTracking(
'not sent with selection', 'set to selection start on forward selection',
(WidgetTester tester) async { (WidgetTester tester) async {
controller.value = TextEditingValue( controller.value = TextEditingValue(
text: 'a' * 100, text: 'a' * 100,
selection: const TextSelection(baseOffset: 0, extentOffset: 10), selection: const TextSelection(baseOffset: 10, extentOffset: 30),
); );
await tester.pumpWidget(builder()); await tester.pumpWidget(builder());
await tester.showKeyboard(find.byType(EditableText)); await tester.showKeyboard(find.byType(EditableText));
expect(tester.testTextInput.log, isNot(contains(matchesMethodCall('TextInput.setCaretRect')))); expect(tester.testTextInput.log, contains(
matchesMethodCall(
'TextInput.setCaretRect',
// Now the composing range is not empty.
args: allOf(
containsPair('x', equals(140)),
containsPair('y', equals(0)),
),
),
));
},
);
testWidgetsWithLeakTracking(
'set to selection start on reversed selection',
(WidgetTester tester) async {
controller.value = TextEditingValue(
text: 'a' * 100,
selection: const TextSelection(baseOffset: 30, extentOffset: 10),
);
await tester.pumpWidget(builder());
await tester.showKeyboard(find.byType(EditableText));
expect(tester.testTextInput.log, contains(
matchesMethodCall(
'TextInput.setCaretRect',
// Now the composing range is not empty.
args: allOf(
containsPair('x', equals(140)),
containsPair('y', equals(0)),
),
),
));
}, },
); );
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment