Unverified Commit 462ad790 authored by Matej Knopp's avatar Matej Knopp Committed by GitHub

Provide caret rect to text input plugin (#77608)

parent 750fd1d5
......@@ -926,6 +926,7 @@ class TextInputConnection {
Size? _cachedSize;
Matrix4? _cachedTransform;
Rect? _cachedRect;
Rect? _cachedCaretRect;
static int _nextId = 1;
final int _id;
......@@ -1028,6 +1029,24 @@ class TextInputConnection {
);
}
/// Sends the coordinates of caret rect. This is used on macOS for positioning
/// the accent selection menu.
void setCaretRect(Rect rect) {
assert(rect != null);
if (rect == _cachedCaretRect)
return;
_cachedCaretRect = rect;
final Rect validRect = rect.isFinite ? rect : Offset.zero & const Size(-1, -1);
TextInput._instance._setCaretRect(
<String, dynamic>{
'width': validRect.width,
'height': validRect.height,
'x': validRect.left,
'y': validRect.top,
},
);
}
/// Send text styling information.
///
/// This information is used by the Flutter Web Engine to change the style
......@@ -1416,6 +1435,13 @@ class TextInput {
);
}
void _setCaretRect(Map<String, dynamic> args) {
_channel.invokeMethod<void>(
'TextInput.setCaretRect',
args,
);
}
void _setStyle(Map<String, dynamic> args) {
_channel.invokeMethod<void>(
'TextInput.setStyle',
......
......@@ -2027,6 +2027,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_textInputConnection!.show();
_updateSizeAndTransform();
_updateComposingRectIfNeeded();
_updateCaretRectIfNeeded();
if (_needsAutofill) {
// Request autofill AFTER the size and the transform have been sent to
// the platform text input plugin.
......@@ -2436,6 +2437,19 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
void _updateCaretRectIfNeeded() {
if (_hasInputConnection) {
if (renderEditable.selection != null && renderEditable.selection!.isValid &&
renderEditable.selection!.isCollapsed) {
final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset);
final Rect caretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
_textInputConnection!.setCaretRect(caretRect);
}
SchedulerBinding.instance!
.addPostFrameCallback((Duration _) => _updateCaretRectIfNeeded());
}
}
TextDirection get _textDirection {
final TextDirection result = widget.textDirection ?? Directionality.of(context);
assert(result != null, '$runtimeType created without a textDirection and with no ambient Directionality.');
......
......@@ -3483,6 +3483,100 @@ void main() {
);
});
group('setCaretRect', () {
Widget builder() {
return MaterialApp(
home: MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: Material(
child: EditableText(
backgroundCursorColor: Colors.grey,
controller: controller,
focusNode: FocusNode(),
style: textStyle,
cursorColor: Colors.blue,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {},
),
),
),
),
),
);
}
testWidgets(
'called with proper coordinates',
(WidgetTester tester) async {
controller.value = TextEditingValue(text: 'a' * 50);
await tester.pumpWidget(builder());
await tester.showKeyboard(find.byType(EditableText));
expect(tester.testTextInput.log, contains(
matchesMethodCall(
'TextInput.setCaretRect',
args: allOf(
// No composing text so the width should not be too wide because
// it's empty.
containsPair('x', equals(700)),
containsPair('y', equals(0)),
containsPair('width', equals(2)),
containsPair('height', equals(14)),
),
),
));
tester.testTextInput.log.clear();
controller.value = TextEditingValue(
text: 'a' * 50, selection: const TextSelection(baseOffset: 0, extentOffset: 0));
await tester.pump();
expect(tester.testTextInput.log, contains(
matchesMethodCall(
'TextInput.setCaretRect',
// Now the composing range is not empty.
args: allOf(
containsPair('x', equals(0)),
containsPair('y', equals(0)),
)
),
));
}, skip: isBrowser); // Related to https://github.com/flutter/flutter/issues/66089
testWidgets(
'only send updates when necessary',
(WidgetTester tester) async {
controller.value = TextEditingValue(text: 'a' * 100);
await tester.pumpWidget(builder());
await tester.showKeyboard(find.byType(EditableText));
expect(tester.testTextInput.log, contains(matchesMethodCall('TextInput.setCaretRect')));
tester.testTextInput.log.clear();
// Should not send updates every frame.
await tester.pump();
expect(tester.testTextInput.log, isNot(contains(matchesMethodCall('TextInput.setCaretRect'))));
});
testWidgets(
'not sent with selection',
(WidgetTester tester) async {
controller.value = TextEditingValue(
text: 'a' * 100, selection: const TextSelection(baseOffset: 0, extentOffset: 10));
await tester.pumpWidget(builder());
await tester.showKeyboard(find.byType(EditableText));
expect(tester.testTextInput.log, isNot(contains(matchesMethodCall('TextInput.setCaretRect'))));
});
});
group('setMarkedTextRect', () {
Widget builder() {
return MaterialApp(
......@@ -5437,6 +5531,7 @@ void main() {
'TextInput.setEditingState',
'TextInput.setEditingState',
'TextInput.show',
'TextInput.setCaretRect',
];
expect(
tester.testTextInput.log.map((MethodCall m) => m.method),
......@@ -5480,6 +5575,7 @@ void main() {
'TextInput.setEditingState',
'TextInput.setEditingState',
'TextInput.show',
'TextInput.setCaretRect',
'TextInput.show',
];
expect(tester.testTextInput.log.length, logOrder.length);
......@@ -5529,6 +5625,7 @@ void main() {
'TextInput.setEditingState',
'TextInput.setEditingState',
'TextInput.show',
'TextInput.setCaretRect',
'TextInput.setEditingState',
];
......
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