Fix cursor outside of input width (#30525)
* Disallow cursor from appearing beyond the width of the input.
* Test that verifies the cursor can't exceed the width of the input
* Use constant from editable.dart to explain 1 pixel difference in test
* Fix failing test that tested the case of overflowing spaces
diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart
index 9c922a9..915b26f 100644
--- a/packages/flutter/lib/src/painting/text_painter.dart
+++ b/packages/flutter/lib/src/painting/text_painter.dart
@@ -486,7 +486,7 @@
final double caretEnd = box.end;
final double dx = box.direction == TextDirection.rtl ? caretEnd - caretPrototype.width : caretEnd;
- return Offset(dx, box.top);
+ return Offset(min(dx, width), box.top);
}
return null;
}
@@ -526,7 +526,7 @@
final TextBox box = boxes.last;
final double caretStart = box.start;
final double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
- return Offset(dx, box.top);
+ return Offset(min(dx, width), box.top);
}
return null;
}
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index d6f47fd..a8811bf 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -426,6 +426,82 @@
}, skip: !Platform.isLinux);
*/
+ testWidgets('Overflowing a line with spaces stops the cursor at the end', (WidgetTester tester) async {
+ final TextEditingController controller = TextEditingController();
+
+ await tester.pumpWidget(
+ overlay(
+ child: TextField(
+ key: textFieldKey,
+ controller: controller,
+ maxLines: null,
+ ),
+ )
+ );
+ expect(controller.selection.baseOffset, -1);
+ expect(controller.selection.extentOffset, -1);
+
+ const String testValueOneLine = 'enough text to be exactly at the end of the line.';
+ await tester.enterText(find.byType(TextField), testValueOneLine);
+ await skipPastScrollingAnimation(tester);
+
+ RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
+
+ RenderBox inputBox = findInputBox();
+ final Size oneLineInputSize = inputBox.size;
+
+ await tester.tapAt(textOffsetToPosition(tester, testValueOneLine.length));
+ await tester.pump();
+
+ const String testValueTwoLines = 'enough text to overflow the first line and go to the second';
+ await tester.enterText(find.byType(TextField), testValueTwoLines);
+ await skipPastScrollingAnimation(tester);
+
+ expect(inputBox, findInputBox());
+ inputBox = findInputBox();
+ expect(inputBox.size.height, greaterThan(oneLineInputSize.height));
+ final Size twoLineInputSize = inputBox.size;
+
+ // Enter a string with the same number of characters as testValueTwoLines,
+ // but where the overflowing part is all spaces. Assert that it only renders
+ // on one line.
+ const String testValueSpaces = testValueOneLine + ' ';
+ expect(testValueSpaces.length, testValueTwoLines.length);
+ await tester.enterText(find.byType(TextField), testValueSpaces);
+ await skipPastScrollingAnimation(tester);
+
+ expect(inputBox, findInputBox());
+ inputBox = findInputBox();
+ expect(inputBox.size.height, oneLineInputSize.height);
+
+ // Swapping the final space for a letter causes it to wrap to 2 lines.
+ const String testValueSpacesOverflow = testValueOneLine + ' a';
+ expect(testValueSpacesOverflow.length, testValueTwoLines.length);
+ await tester.enterText(find.byType(TextField), testValueSpacesOverflow);
+ await skipPastScrollingAnimation(tester);
+
+ expect(inputBox, findInputBox());
+ inputBox = findInputBox();
+ expect(inputBox.size.height, twoLineInputSize.height);
+
+ // Positioning the cursor at the end of a line overflowing with spaces puts
+ // it inside the input still.
+ await tester.enterText(find.byType(TextField), testValueSpaces);
+ await skipPastScrollingAnimation(tester);
+ await tester.tapAt(textOffsetToPosition(tester, testValueSpaces.length));
+ await tester.pump();
+
+ final double inputWidth = findRenderEditable(tester).size.width;
+ final Offset cursorOffsetSpaces = findRenderEditable(tester).getLocalRectForCaret(
+ const TextPosition(offset: testValueSpaces.length),
+ ).bottomRight;
+
+ // Gap between caret and edge of input, defined in editable.dart.
+ const int _kCaretGap = 1;
+
+ expect(cursorOffsetSpaces.dx, inputWidth - _kCaretGap);
+ });
+
testWidgets('obscureText control test', (WidgetTester tester) async {
await tester.pumpWidget(
overlay(
diff --git a/packages/flutter/test/painting/text_painter_test.dart b/packages/flutter/test/painting/text_painter_test.dart
index e5f65a9..80d0c22 100644
--- a/packages/flutter/test/painting/text_painter_test.dart
+++ b/packages/flutter/test/painting/text_painter_test.dart
@@ -133,7 +133,9 @@
Offset caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 0), ui.Rect.zero);
expect(caretOffset.dx, 21);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: text.length), ui.Rect.zero);
- expect(caretOffset.dx, 441);
+ // The end of the line is 441, but the width is only 420, so the cursor is
+ // stopped there without overflowing.
+ expect(caretOffset.dx, painter.width);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 1), ui.Rect.zero);
expect(caretOffset.dx, 35);