diff --git a/packages/flutter/lib/src/painting/matrix_utils.dart b/packages/flutter/lib/src/painting/matrix_utils.dart index 56695ce2f2..643a6a5cc1 100644 --- a/packages/flutter/lib/src/painting/matrix_utils.dart +++ b/packages/flutter/lib/src/painting/matrix_utils.dart @@ -127,6 +127,13 @@ class MatrixUtils { /// /// This function assumes the given point has a z-coordinate of 0.0. The /// z-coordinate of the result is ignored. + /// + /// While not common, this method may return (NaN, NaN), iff the given `point` + /// results in a "point at infinity" in homogeneous coordinates after applying + /// the `transform`. For example, a [RenderObject] may set its transform to + /// the zero matrix to indicate its content is currently not visible. Trying + /// to convert an `Offset` to its coordinate space always results in + /// (NaN, NaN). static Offset transformPoint(Matrix4 transform, Offset point) { final Float64List storage = transform.storage; final double x = point.dx; diff --git a/packages/flutter/lib/src/rendering/box.dart b/packages/flutter/lib/src/rendering/box.dart index 2f3a82dd3a..ef1db05d02 100644 --- a/packages/flutter/lib/src/rendering/box.dart +++ b/packages/flutter/lib/src/rendering/box.dart @@ -2293,7 +2293,9 @@ abstract class RenderBox extends RenderObject { /// coordinate system of `ancestor` (which must be an ancestor of this render /// object) instead of to the global coordinate system. /// - /// This method is implemented in terms of [getTransformTo]. + /// This method is implemented in terms of [getTransformTo]. If the transform + /// matrix puts the given `point` on the line at infinity (for instance, when + /// the transform matrix is the zero matrix), this method returns (NaN, NaN). Offset localToGlobal(Offset point, { RenderObject ancestor }) { return MatrixUtils.transformPoint(getTransformTo(ancestor), point); } diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart index 1bc5a51acc..9bd125ca5c 100644 --- a/packages/flutter/lib/src/rendering/editable.dart +++ b/packages/flutter/lib/src/rendering/editable.dart @@ -1936,10 +1936,12 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin { Offset _getPixelPerfectCursorOffset(Rect caretRect) { final Offset caretPosition = localToGlobal(caretRect.topLeft); final double pixelMultiple = 1.0 / _devicePixelRatio; - final int quotientX = (caretPosition.dx / pixelMultiple).round(); - final int quotientY = (caretPosition.dy / pixelMultiple).round(); - final double pixelPerfectOffsetX = quotientX * pixelMultiple - caretPosition.dx; - final double pixelPerfectOffsetY = quotientY * pixelMultiple - caretPosition.dy; + final double pixelPerfectOffsetX = caretPosition.dx.isFinite + ? (caretPosition.dx / pixelMultiple).round() * pixelMultiple - caretPosition.dx + : caretPosition.dx; + final double pixelPerfectOffsetY = caretPosition.dy.isFinite + ? (caretPosition.dy / pixelMultiple).round() * pixelMultiple - caretPosition.dy + : caretPosition.dy; return Offset(pixelPerfectOffsetX, pixelPerfectOffsetY); } diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index c16849e697..51e4263089 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -4321,6 +4321,30 @@ void main() { expect(scrollController.offset, 0); }); + testWidgets('getLocalRectForCaret does not throw when it sees an infinite point', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: SkipPainting( + child: Transform( + transform: Matrix4.zero(), + child: EditableText( + controller: TextEditingController(), + focusNode: FocusNode(), + style: textStyle, + cursorColor: Colors.blue, + backgroundCursorColor: Colors.grey, + ), + ), + ), + ), + ); + + final EditableTextState state = tester.state(find.byType(EditableText)); + final Rect rect = state.renderEditable.getLocalRectForCaret(const TextPosition(offset: 0)); + expect(rect.isFinite, false); + expect(tester.takeException(), isNull); + }); + testWidgets('obscured multiline fields throw an exception', (WidgetTester tester) async { final TextEditingController controller = TextEditingController(); expect( @@ -5350,3 +5374,15 @@ class NoImplicitScrollPhysics extends AlwaysScrollableScrollPhysics { return NoImplicitScrollPhysics(parent: buildParent(ancestor)); } } + +class SkipPainting extends SingleChildRenderObjectWidget { + const SkipPainting({ Key key, Widget child }): super(key: key, child: child); + + @override + SkipPaintingRenderObject createRenderObject(BuildContext context) => SkipPaintingRenderObject(); +} + +class SkipPaintingRenderObject extends RenderProxyBox { + @override + void paint(PaintingContext context, Offset offset) { } +}