Skip to content

Commit 8276375

Browse files
cbrackenpull[bot]
authored andcommitted
Send caret rect to embedder on selection update (flutter#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: flutter#137677
1 parent b5722b9 commit 8276375

File tree

2 files changed

+56
-10
lines changed

2 files changed

+56
-10
lines changed

packages/flutter/lib/src/widgets/editable_text.dart

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4115,11 +4115,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
41154115
_textInputConnection!.setSelectionRects(rects);
41164116
}
41174117

4118-
// Sends the current composing rect to the iOS text input plugin via the text
4119-
// input channel. We need to keep sending the information even if no text is
4120-
// currently marked, as the information usually lags behind. The text input
4121-
// plugin needs to estimate the composing rect based on the latest caret rect,
4122-
// when the composing rect info didn't arrive in time.
4118+
// Sends the current composing rect to the embedder's text input plugin.
4119+
//
4120+
// In cases where the composing rect hasn't been updated in the embedder due
4121+
// to the lag of asynchronous messages over the channel, the position of the
4122+
// current caret rect is used instead.
4123+
//
4124+
// See: [_updateCaretRectIfNeeded]
41234125
void _updateComposingRectIfNeeded() {
41244126
final TextRange composingRange = _value.composing;
41254127
assert(mounted);
@@ -4133,12 +4135,24 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
41334135
_textInputConnection!.setComposingRect(composingRect);
41344136
}
41354137

4138+
// Sends the current caret rect to the embedder's text input plugin.
4139+
//
4140+
// The position of the caret rect is updated periodically such that if the
4141+
// user initiates composing input, the current cursor rect can be used for
4142+
// the first character until the composing rect can be sent.
4143+
//
4144+
// On selection changes, the start of the selection is used. This ensures
4145+
// that regardless of the direction the selection was created, the cursor is
4146+
// set to the position where next text input occurs. This position is used to
4147+
// position the IME's candidate selection menu.
4148+
//
4149+
// See: [_updateComposingRectIfNeeded]
41364150
void _updateCaretRectIfNeeded() {
41374151
final TextSelection? selection = renderEditable.selection;
4138-
if (selection == null || !selection.isValid || !selection.isCollapsed) {
4152+
if (selection == null || !selection.isValid) {
41394153
return;
41404154
}
4141-
final TextPosition currentTextPosition = TextPosition(offset: selection.baseOffset);
4155+
final TextPosition currentTextPosition = TextPosition(offset: selection.start);
41424156
final Rect caretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
41434157
_textInputConnection!.setCaretRect(caretRect);
41444158
}

packages/flutter/test/widgets/editable_text_test.dart

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5875,16 +5875,48 @@ void main() {
58755875
);
58765876

58775877
testWidgetsWithLeakTracking(
5878-
'not sent with selection',
5878+
'set to selection start on forward selection',
58795879
(WidgetTester tester) async {
58805880
controller.value = TextEditingValue(
58815881
text: 'a' * 100,
5882-
selection: const TextSelection(baseOffset: 0, extentOffset: 10),
5882+
selection: const TextSelection(baseOffset: 10, extentOffset: 30),
58835883
);
58845884
await tester.pumpWidget(builder());
58855885
await tester.showKeyboard(find.byType(EditableText));
58865886

5887-
expect(tester.testTextInput.log, isNot(contains(matchesMethodCall('TextInput.setCaretRect'))));
5887+
expect(tester.testTextInput.log, contains(
5888+
matchesMethodCall(
5889+
'TextInput.setCaretRect',
5890+
// Now the composing range is not empty.
5891+
args: allOf(
5892+
containsPair('x', equals(140)),
5893+
containsPair('y', equals(0)),
5894+
),
5895+
),
5896+
));
5897+
},
5898+
);
5899+
5900+
testWidgetsWithLeakTracking(
5901+
'set to selection start on reversed selection',
5902+
(WidgetTester tester) async {
5903+
controller.value = TextEditingValue(
5904+
text: 'a' * 100,
5905+
selection: const TextSelection(baseOffset: 30, extentOffset: 10),
5906+
);
5907+
await tester.pumpWidget(builder());
5908+
await tester.showKeyboard(find.byType(EditableText));
5909+
5910+
expect(tester.testTextInput.log, contains(
5911+
matchesMethodCall(
5912+
'TextInput.setCaretRect',
5913+
// Now the composing range is not empty.
5914+
args: allOf(
5915+
containsPair('x', equals(140)),
5916+
containsPair('y', equals(0)),
5917+
),
5918+
),
5919+
));
58885920
},
58895921
);
58905922
});

0 commit comments

Comments
 (0)