From e84daf3b709d8f5d8a6aa795436eea34e698bce5 Mon Sep 17 00:00:00 2001 From: Callum Moffat Date: Fri, 21 Apr 2023 21:40:20 -0400 Subject: [PATCH] Adjust selection rects inclusion criteria (#125022) Include rects with any overlap instead of only when top-left or bottom-right included. The previous criteria didn't send any selection rects when text was taller than the text box and scroll offset was not zero. Part of #30476 --- .../lib/src/widgets/editable_text.dart | 6 +- .../test/widgets/editable_text_test.dart | 74 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index 6ef90359ef..3d97ce81c1 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -3848,7 +3848,11 @@ class EditableTextState extends State with AutomaticKeepAliveClien if (paintBounds.bottom <= box.top) { break; } - if (paintBounds.contains(Offset(box.left, box.top)) || paintBounds.contains(Offset(box.right, box.bottom))) { + // Include any TextBox which intersects with the RenderEditable. + if (paintBounds.left <= box.right && + box.left <= paintBounds.right && + paintBounds.top <= box.bottom) { + // At least some part of the letter is visible within the text field. rects.add(SelectionRect(position: graphemeStart, bounds: box.toRect(), direction: box.direction)); } } diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index 1f81da49cb..170b5d9db9 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -5398,6 +5398,80 @@ void main() { // On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects. }, skip: kIsWeb, variant: const TargetPlatformVariant({ TargetPlatform.iOS })); // [intended] + testWidgets('selection rects sent even when character corners are outside of paintBounds', (WidgetTester tester) async { + final List> log = >[]; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) { + if (methodCall.method == 'TextInput.setSelectionRects') { + final List args = methodCall.arguments as List; + final List selectionRects = []; + for (final dynamic rect in args) { + selectionRects.add(SelectionRect( + position: (rect as List)[4] as int, + bounds: Rect.fromLTWH(rect[0] as double, rect[1] as double, rect[2] as double, rect[3] as double), + )); + } + log.add(selectionRects); + } + return null; + }); + + final TextEditingController controller = TextEditingController(); + final ScrollController scrollController = ScrollController(); + controller.text = 'Text1'; + + final GlobalKey editableTextKey = GlobalKey(); + + Future pumpEditableText({ double? width, double? height, TextAlign textAlign = TextAlign.start }) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(), + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: SizedBox( + width: width, + height: height, + child: EditableText( + controller: controller, + textAlign: textAlign, + scrollController: scrollController, + maxLines: null, + focusNode: focusNode, + cursorWidth: 0, + key: editableTextKey, + style: Typography.material2018().black.titleMedium!, + cursorColor: Colors.blue, + backgroundCursorColor: Colors.grey, + ), + ), + ), + ), + ), + ); + } + + // Set height to 1 pixel less than full height. + await pumpEditableText(height: 13); + expect(log, isEmpty); + + // Scroll so that the top of each character is above the top of the renderEditable + // and the bottom of each character is below the bottom of the renderEditable. + editableTextKey.currentState!.renderEditable.offset = ViewportOffset.fixed(0.5); + + await tester.showKeyboard(find.byType(EditableText)); + // We should get all the rects. + expect(log.single, const [ + SelectionRect(position: 0, bounds: Rect.fromLTRB(0.0, -0.5, 14.0, 13.5)), + SelectionRect(position: 1, bounds: Rect.fromLTRB(14.0, -0.5, 28.0, 13.5)), + SelectionRect(position: 2, bounds: Rect.fromLTRB(28.0, -0.5, 42.0, 13.5)), + SelectionRect(position: 3, bounds: Rect.fromLTRB(42.0, -0.5, 56.0, 13.5)), + SelectionRect(position: 4, bounds: Rect.fromLTRB(56.0, -0.5, 70.0, 13.5)) + ]); + log.clear(); + + // On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects. + }, skip: kIsWeb, variant: const TargetPlatformVariant({ TargetPlatform.iOS })); // [intended] + testWidgets('text styling info is sent on show keyboard', (WidgetTester tester) async { final List log = []; tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall methodCall) async {