diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index d03d5f6b2d..7952912b1b 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -1916,9 +1916,9 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, @override @protected bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { + final Offset effectivePosition = position - _paintOffset; final InlineSpan? textSpan = _textPainter.text; if (textSpan != null) { - final Offset effectivePosition = position - _paintOffset; final TextPosition textPosition = _textPainter.getPositionForOffset(effectivePosition); final Object? span = textSpan.getSpanForPosition(textPosition); if (span is HitTestTarget) { @@ -1926,7 +1926,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, return true; } } - return hitTestInlineChildren(result, position); + return hitTestInlineChildren(result, effectivePosition); } late TapGestureRecognizer _tap; diff --git a/packages/flutter/test/rendering/editable_test.dart b/packages/flutter/test/rendering/editable_test.dart index 118f1adbf6..d859ec1c63 100644 --- a/packages/flutter/test/rendering/editable_test.dart +++ b/packages/flutter/test/rendering/editable_test.dart @@ -1721,6 +1721,89 @@ void main() { editable.hitTest(result, position: const Offset(5.0, 15.0)); expect(result.path, hasLength(0)); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020 + + test('hits correct WidgetSpan when scrolled', () { + final String text = '${"\n" * 10}test'; + final TextSelectionDelegate delegate = _FakeEditableTextState() + ..textEditingValue = TextEditingValue( + text: text, + selection: const TextSelection.collapsed(offset: 13), + ); + final List renderBoxes = [ + RenderParagraph(const TextSpan(text: 'a'), textDirection: TextDirection.ltr), + RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr), + RenderParagraph(const TextSpan(text: 'c'), textDirection: TextDirection.ltr), + ]; + final RenderEditable editable = RenderEditable( + maxLines: null, + text: TextSpan( + style: const TextStyle(height: 1.0, fontSize: 10.0), + children: [ + TextSpan(text: text), + const WidgetSpan(child: Text('a')), + const TextSpan(children: [ + WidgetSpan(child: Text('b')), + WidgetSpan(child: Text('c')), + ], + ), + ], + ), + startHandleLayerLink: LayerLink(), + endHandleLayerLink: LayerLink(), + textDirection: TextDirection.ltr, + offset: ViewportOffset.fixed(100.0), // equal to the height of the 10 empty lines + textSelectionDelegate: delegate, + selection: const TextSelection.collapsed( + offset: 0, + ), + children: renderBoxes, + ); + _applyParentData(renderBoxes, editable.text!); + layout(editable, constraints: BoxConstraints.loose(const Size(500.0, 500.0))); + // Prepare for painting after layout. + pumpFrame(phase: EnginePhase.compositingBits); + BoxHitTestResult result = BoxHitTestResult(); + editable.hitTest(result, position: Offset.zero); + // We expect two hit test entries in the path because the RenderEditable + // will add itself as well. + expect(result.path, hasLength(2)); + HitTestTarget target = result.path.first.target; + expect(target, isA()); + expect((target as TextSpan).text, text); + // Only testing the RenderEditable entry here once, not anymore below. + expect(result.path.last.target, isA()); + result = BoxHitTestResult(); + editable.hitTest(result, position: const Offset(15.0, 0.0)); + expect(result.path, hasLength(2)); + target = result.path.first.target; + expect(target, isA()); + expect((target as TextSpan).text, text); + + result = BoxHitTestResult(); + editable.hitTest(result, position: const Offset(41.0, 0.0)); + expect(result.path, hasLength(3)); + target = result.path.first.target; + expect(target, isA()); + expect((target as TextSpan).text, 'a'); + + result = BoxHitTestResult(); + editable.hitTest(result, position: const Offset(55.0, 0.0)); + expect(result.path, hasLength(3)); + target = result.path.first.target; + expect(target, isA()); + expect((target as TextSpan).text, 'b'); + + result = BoxHitTestResult(); + editable.hitTest(result, position: const Offset(69.0, 5.0)); + expect(result.path, hasLength(3)); + target = result.path.first.target; + expect(target, isA()); + expect((target as TextSpan).text, 'c'); + + result = BoxHitTestResult(); + editable.hitTest(result, position: const Offset(5.0, 15.0)); + expect(result.path, hasLength(2)); + }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020 }); test('does not skip TextPainter.layout because of invalid cache', () {