Fix Caret Height On Empty Lines (#120834)
* improve caret caching, fix caret for empty text/line, `getLocalRectForCaret` now reports the real rect that will be painted.
move caret x-coordinate clamping to RenderEditable since TextPainter doesn't know about clipping.
* comments
* review
diff --git a/packages/flutter/lib/src/painting/text_painter.dart b/packages/flutter/lib/src/painting/text_painter.dart
index 29e5fdf..70d8054 100644
--- a/packages/flutter/lib/src/painting/text_painter.dart
+++ b/packages/flutter/lib/src/painting/text_painter.dart
@@ -273,14 +273,32 @@
/// This is used to cache and pass the computed metrics regarding the
/// caret's size and position. This is preferred due to the expensive
/// nature of the calculation.
-class _CaretMetrics {
- const _CaretMetrics({required this.offset, this.fullHeight});
+///
+// This should be a sealed class: A _CaretMetrics is either a _LineCaretMetrics
+// or an _EmptyLineCaretMetrics.
+@immutable
+abstract class _CaretMetrics { }
+
+/// The _CaretMetrics for carets located in a non-empty line. Carets located in a
+/// non-empty line are associated with a glyph within the same line.
+class _LineCaretMetrics implements _CaretMetrics {
+ const _LineCaretMetrics({required this.offset, required this.writingDirection, required this.fullHeight});
/// The offset of the top left corner of the caret from the top left
/// corner of the paragraph.
final Offset offset;
-
+ /// The writing direction of the glyph the _CaretMetrics is associated with.
+ final TextDirection writingDirection;
/// The full height of the glyph at the caret position.
- final double? fullHeight;
+ final double fullHeight;
+}
+
+/// The _CaretMetrics for carets located in an empty line (when the text is
+/// empty, or the caret is between two a newline characters).
+class _EmptyLineCaretMetrics implements _CaretMetrics {
+ const _EmptyLineCaretMetrics({ required this.lineVerticalOffset });
+
+ /// The y offset of the unoccupied line.
+ final double lineVerticalOffset;
}
/// An object that paints a [TextSpan] tree into a [Canvas].
@@ -466,7 +484,6 @@
_paragraph = null;
_lineMetricsCache = null;
_previousCaretPosition = null;
- _previousCaretPrototype = null;
}
/// The (potentially styled) text to paint.
@@ -935,7 +952,6 @@
// A change in layout invalidates the cached caret and line metrics as well.
_lineMetricsCache = null;
_previousCaretPosition = null;
- _previousCaretPrototype = null;
_layoutParagraph(minWidth, maxWidth);
_inlinePlaceholderBoxes = _paragraph!.getBoxesForPlaceholders();
}
@@ -1027,9 +1043,9 @@
// Unicode value for a zero width joiner character.
static const int _zwjUtf16 = 0x200d;
- // Get the Rect of the cursor (in logical pixels) based off the near edge
- // of the character upstream from the given string offset.
- Rect? _getRectFromUpstream(int offset, Rect caretPrototype) {
+ // Get the caret metrics (in logical pixels) based off the near edge of the
+ // character upstream from the given string offset.
+ _CaretMetrics? _getMetricsFromUpstream(int offset) {
final int plainTextLength = plainText.length;
if (plainTextLength == 0 || offset > plainTextLength) {
return null;
@@ -1067,21 +1083,15 @@
}
final TextBox box = boxes.first;
- if (prevCodeUnit == NEWLINE_CODE_UNIT) {
- return Rect.fromLTRB(_emptyOffset.dx, box.bottom, _emptyOffset.dx, box.bottom + box.bottom - box.top);
- }
-
- final double caretEnd = box.end;
- final double dx = box.direction == TextDirection.rtl ? caretEnd - caretPrototype.width : caretEnd;
- return Rect.fromLTRB(clampDouble(dx, 0, _paragraph!.width), box.top,
- clampDouble(dx, 0, _paragraph!.width), box.bottom);
+ return prevCodeUnit == NEWLINE_CODE_UNIT
+ ? _EmptyLineCaretMetrics(lineVerticalOffset: box.bottom)
+ : _LineCaretMetrics(offset: Offset(box.end, box.top), writingDirection: box.direction, fullHeight: box.bottom - box.top);
}
return null;
}
-
- // Get the Rect of the cursor (in logical pixels) based off the near edge
- // of the character downstream from the given string offset.
- Rect? _getRectFromDownstream(int offset, Rect caretPrototype) {
+ // Get the caret metrics (in logical pixels) based off the near edge of the
+ // character downstream from the given string offset.
+ _CaretMetrics? _getMetricsFromDownstream(int offset) {
final int plainTextLength = plainText.length;
if (plainTextLength == 0 || offset < 0) {
return null;
@@ -1116,38 +1126,33 @@
continue;
}
final TextBox box = boxes.last;
- final double caretStart = box.start;
- final double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
- return Rect.fromLTRB(clampDouble(dx, 0, _paragraph!.width), box.top, clampDouble(dx, 0, _paragraph!.width), box.bottom);
+ return _LineCaretMetrics(offset: Offset(box.start, box.top), writingDirection: box.direction, fullHeight: box.bottom - box.top);
}
return null;
}
- Offset get _emptyOffset {
- assert(_debugAssertTextLayoutIsValid); // implies textDirection is non-null
+ static double _computePaintOffsetFraction(TextAlign textAlign, TextDirection textDirection) {
switch (textAlign) {
case TextAlign.left:
- return Offset.zero;
+ return 0.0;
case TextAlign.right:
- return Offset(width, 0.0);
+ return 1.0;
case TextAlign.center:
- return Offset(width / 2.0, 0.0);
- case TextAlign.justify:
+ return 0.5;
case TextAlign.start:
- assert(textDirection != null);
- switch (textDirection!) {
+ case TextAlign.justify:
+ switch (textDirection) {
case TextDirection.rtl:
- return Offset(width, 0.0);
+ return 1.0;
case TextDirection.ltr:
- return Offset.zero;
+ return 0.0;
}
case TextAlign.end:
- assert(textDirection != null);
- switch (textDirection!) {
+ switch (textDirection) {
case TextDirection.rtl:
- return Offset.zero;
+ return 0.0;
case TextDirection.ltr:
- return Offset(width, 0.0);
+ return 1.0;
}
}
}
@@ -1156,8 +1161,33 @@
///
/// Valid only after [layout] has been called.
Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
- _computeCaretMetrics(position, caretPrototype);
- return _caretMetrics.offset;
+ final _CaretMetrics caretMetrics = _computeCaretMetrics(position);
+
+ if (caretMetrics is _EmptyLineCaretMetrics) {
+ final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection!);
+ // The full width is not (width - caretPrototype.width)
+ // because RenderEditable reserves cursor width on the right. Ideally this
+ // should be handled by RenderEditable instead.
+ final double dx = paintOffsetAlignment == 0 ? 0 : paintOffsetAlignment * width;
+ return Offset(dx, caretMetrics.lineVerticalOffset);
+ }
+
+ final Offset offset;
+ switch ((caretMetrics as _LineCaretMetrics).writingDirection) {
+ case TextDirection.rtl:
+ offset = Offset(caretMetrics.offset.dx - caretPrototype.width, caretMetrics.offset.dy);
+ break;
+ case TextDirection.ltr:
+ offset = caretMetrics.offset;
+ break;
+ }
+ // If offset.dx is outside of the advertised content area, then the associated
+ // glyph cluster belongs to a trailing newline character. Ideally the behavior
+ // should be handled by higher-level implementations (for instance,
+ // RenderEditable reserves width for showing the caret, it's best to handle
+ // the clamping there).
+ final double adjustedDx = clampDouble(offset.dx, 0, width);
+ return Offset(adjustedDx, offset.dy);
}
/// {@template flutter.painting.textPainter.getFullHeightForCaret}
@@ -1166,8 +1196,8 @@
///
/// Valid only after [layout] has been called.
double? getFullHeightForCaret(TextPosition position, Rect caretPrototype) {
- _computeCaretMetrics(position, caretPrototype);
- return _caretMetrics.fullHeight;
+ final _CaretMetrics caretMetrics = _computeCaretMetrics(position);
+ return caretMetrics is _LineCaretMetrics ? caretMetrics.fullHeight : null;
}
// Cached caret metrics. This allows multiple invokes of [getOffsetForCaret] and
@@ -1179,35 +1209,29 @@
// computed with. When new values are passed in, we recompute the caret metrics.
// only as necessary.
TextPosition? _previousCaretPosition;
- Rect? _previousCaretPrototype;
// Checks if the [position] and [caretPrototype] have changed from the cached
// version and recomputes the metrics required to position the caret.
- void _computeCaretMetrics(TextPosition position, Rect caretPrototype) {
+ _CaretMetrics _computeCaretMetrics(TextPosition position) {
assert(_debugAssertTextLayoutIsValid);
- if (position == _previousCaretPosition && caretPrototype == _previousCaretPrototype) {
- return;
+ if (position == _previousCaretPosition) {
+ return _caretMetrics;
}
final int offset = position.offset;
- Rect? rect;
+ final _CaretMetrics? metrics;
switch (position.affinity) {
case TextAffinity.upstream: {
- rect = _getRectFromUpstream(offset, caretPrototype) ?? _getRectFromDownstream(offset, caretPrototype);
+ metrics = _getMetricsFromUpstream(offset) ?? _getMetricsFromDownstream(offset);
break;
}
case TextAffinity.downstream: {
- rect = _getRectFromDownstream(offset, caretPrototype) ?? _getRectFromUpstream(offset, caretPrototype);
+ metrics = _getMetricsFromDownstream(offset) ?? _getMetricsFromUpstream(offset);
break;
}
}
- _caretMetrics = _CaretMetrics(
- offset: rect != null ? Offset(rect.left, rect.top) : _emptyOffset,
- fullHeight: rect != null ? rect.bottom - rect.top : null,
- );
-
// Cache the input parameters to prevent repeat work later.
_previousCaretPosition = position;
- _previousCaretPrototype = caretPrototype;
+ return _caretMetrics = metrics ?? const _EmptyLineCaretMetrics(lineVerticalOffset: 0);
}
/// Returns a list of rects that bound the given selection.
diff --git a/packages/flutter/lib/src/painting/text_span.dart b/packages/flutter/lib/src/painting/text_span.dart
index e647071..10b7068 100644
--- a/packages/flutter/lib/src/painting/text_span.dart
+++ b/packages/flutter/lib/src/painting/text_span.dart
@@ -406,14 +406,14 @@
@override
int? codeUnitAtVisitor(int index, Accumulator offset) {
+ final String? text = this.text;
if (text == null) {
return null;
}
- if (index - offset.value < text!.length) {
- return text!.codeUnitAt(index - offset.value);
- }
- offset.increment(text!.length);
- return null;
+ final int localOffset = index - offset.value;
+ assert(localOffset >= 0);
+ offset.increment(text.length);
+ return localOffset < text.length ? text.codeUnitAt(localOffset) : null;
}
/// Populates the `semanticsOffsets` and `semanticsElements` with the appropriate data
diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart
index c3ad204..fe7e859 100644
--- a/packages/flutter/lib/src/rendering/editable.dart
+++ b/packages/flutter/lib/src/rendering/editable.dart
@@ -1790,11 +1790,46 @@
/// [TextPainter] object.
Rect getLocalRectForCaret(TextPosition caretPosition) {
_computeTextMetricsIfNeeded();
- final Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype);
- // This rect is the same as _caretPrototype but without the vertical padding.
- final Rect rect = Rect.fromLTWH(0.0, 0.0, cursorWidth, cursorHeight).shift(caretOffset + _paintOffset + cursorOffset);
- // Add additional cursor offset (generally only if on iOS).
- return rect.shift(_snapToPhysicalPixel(rect.topLeft));
+ final Rect caretPrototype = _caretPrototype;
+ final Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, caretPrototype);
+ Rect caretRect = caretPrototype.shift(caretOffset + cursorOffset);
+ final double scrollableWidth = math.max(_textPainter.width + _caretMargin, size.width);
+
+ final double caretX = clampDouble(caretRect.left, 0, math.max(scrollableWidth - _caretMargin, 0));
+ caretRect = Offset(caretX, caretRect.top) & caretRect.size;
+
+ final double caretHeight = cursorHeight;
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.iOS:
+ case TargetPlatform.macOS:
+ final double fullHeight = _textPainter.getFullHeightForCaret(caretPosition, caretPrototype) ?? _textPainter.preferredLineHeight;
+ final double heightDiff = fullHeight - caretRect.height;
+ // Center the caret vertically along the text.
+ caretRect = Rect.fromLTWH(
+ caretRect.left,
+ caretRect.top + heightDiff / 2,
+ caretRect.width,
+ caretRect.height,
+ );
+ break;
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ case TargetPlatform.linux:
+ case TargetPlatform.windows:
+ // Override the height to take the full height of the glyph at the TextPosition
+ // when not on iOS. iOS has special handling that creates a taller caret.
+ // TODO(garyq): See the TODO for _computeCaretPrototype().
+ caretRect = Rect.fromLTWH(
+ caretRect.left,
+ caretRect.top - _kCaretHeightOffset,
+ caretRect.width,
+ caretHeight,
+ );
+ break;
+ }
+
+ caretRect = caretRect.shift(_paintOffset);
+ return caretRect.shift(_snapToPhysicalPixel(caretRect.topLeft));
}
@override
@@ -2311,13 +2346,7 @@
late Rect _caretPrototype;
- // TODO(garyq): This is no longer producing the highest-fidelity caret
- // heights for Android, especially when non-alphabetic languages
- // are involved. The current implementation overrides the height set
- // here with the full measured height of the text on Android which looks
- // superior (subjectively and in terms of fidelity) in _paintCaret. We
- // should rework this properly to once again match the platform. The constant
- // _kCaretHeightOffset scales poorly for small font sizes.
+ // TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/120836
//
/// On iOS, the cursor is taller than the cursor on Android. The height
/// of the cursor for iOS is approximate and obtained through an eyeball
@@ -2970,44 +2999,7 @@
}
void paintRegularCursor(Canvas canvas, RenderEditable renderEditable, Color caretColor, TextPosition textPosition) {
- final Rect caretPrototype = renderEditable._caretPrototype;
- final Offset caretOffset = renderEditable._textPainter.getOffsetForCaret(textPosition, caretPrototype);
- Rect caretRect = caretPrototype.shift(caretOffset + cursorOffset);
-
- final double? caretHeight = renderEditable._textPainter.getFullHeightForCaret(textPosition, caretPrototype);
- if (caretHeight != null) {
- switch (defaultTargetPlatform) {
- case TargetPlatform.iOS:
- case TargetPlatform.macOS:
- final double heightDiff = caretHeight - caretRect.height;
- // Center the caret vertically along the text.
- caretRect = Rect.fromLTWH(
- caretRect.left,
- caretRect.top + heightDiff / 2,
- caretRect.width,
- caretRect.height,
- );
- break;
- case TargetPlatform.android:
- case TargetPlatform.fuchsia:
- case TargetPlatform.linux:
- case TargetPlatform.windows:
- // Override the height to take the full height of the glyph at the TextPosition
- // when not on iOS. iOS has special handling that creates a taller caret.
- // TODO(garyq): See the TODO for _computeCaretPrototype().
- caretRect = Rect.fromLTWH(
- caretRect.left,
- caretRect.top - _kCaretHeightOffset,
- caretRect.width,
- caretHeight,
- );
- break;
- }
- }
-
- caretRect = caretRect.shift(renderEditable._paintOffset);
- final Rect integralRect = caretRect.shift(renderEditable._snapToPhysicalPixel(caretRect.topLeft));
-
+ final Rect integralRect = renderEditable.getLocalRectForCaret(textPosition);
if (shouldPaint) {
final Radius? radius = cursorRadius;
caretPaint.color = caretColor;
diff --git a/packages/flutter/lib/src/widgets/widget_span.dart b/packages/flutter/lib/src/widgets/widget_span.dart
index 94f795a..64f8912 100644
--- a/packages/flutter/lib/src/widgets/widget_span.dart
+++ b/packages/flutter/lib/src/widgets/widget_span.dart
@@ -136,8 +136,10 @@
@override
int? codeUnitAtVisitor(int index, Accumulator offset) {
+ final int localOffset = index - offset.value;
+ assert(localOffset >= 0);
offset.increment(1);
- return PlaceholderSpan.placeholderCodeUnit;
+ return localOffset == 0 ? PlaceholderSpan.placeholderCodeUnit : null;
}
@override
diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart
index da1a866..ad8f75c 100644
--- a/packages/flutter/test/cupertino/text_field_test.dart
+++ b/packages/flutter/test/cupertino/text_field_test.dart
@@ -3108,7 +3108,7 @@
);
expect(firstCharEndpoint.length, 1);
// The first character is now offscreen to the left.
- expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-309.30, epsilon: 1));
+ expect(firstCharEndpoint[0].point.dx, moreOrLessEquals(-310.30, epsilon: 1));
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('long press drag can edge scroll on Apple platforms', (WidgetTester tester) async {
@@ -4813,7 +4813,7 @@
// The ListView has scrolled to keep the TextField and cursor handle
// visible.
- expect(scrollController.offset, 25.0);
+ expect(scrollController.offset, 27.0);
});
testWidgets('disabled state golden', (WidgetTester tester) async {
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index 644ab62..0da0bbb 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -4960,6 +4960,7 @@
);
final RenderEditable editable = findRenderEditable(tester);
+ assert(editable.size.width == 300);
Offset topLeft = editable.localToGlobal(
editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft,
);
@@ -11765,7 +11766,7 @@
// The ListView has scrolled to keep the TextField and cursor handle
// visible.
- expect(scrollController.offset, 48.0);
+ expect(scrollController.offset, 50.0);
});
// Regression test for https://github.com/flutter/flutter/issues/74566
@@ -11798,7 +11799,7 @@
await tester.pumpAndSettle();
// The ListView has scrolled to keep the TextField visible.
- expect(scrollController.offset, 48.0);
+ expect(scrollController.offset, 50.0);
expect(textFieldScrollController.offset, 0.0);
// After entering some long text, the last input character remains on the screen.
diff --git a/packages/flutter/test/painting/widget_span_test.dart b/packages/flutter/test/painting/widget_span_test.dart
new file mode 100644
index 0000000..6329207
--- /dev/null
+++ b/packages/flutter/test/painting/widget_span_test.dart
@@ -0,0 +1,28 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ test('WidgetSpan codeUnitAt', () {
+ const InlineSpan span = WidgetSpan(child: SizedBox());
+ expect(span.codeUnitAt(-1), isNull);
+ expect(span.codeUnitAt(0), PlaceholderSpan.placeholderCodeUnit);
+ expect(span.codeUnitAt(1), isNull);
+ expect(span.codeUnitAt(2), isNull);
+
+ const InlineSpan nestedSpan = TextSpan(
+ text: 'AAA',
+ children: <InlineSpan>[span, span],
+ );
+ expect(nestedSpan.codeUnitAt(-1), isNull);
+ expect(nestedSpan.codeUnitAt(0), 65);
+ expect(nestedSpan.codeUnitAt(1), 65);
+ expect(nestedSpan.codeUnitAt(2), 65);
+ expect(nestedSpan.codeUnitAt(3), PlaceholderSpan.placeholderCodeUnit);
+ expect(nestedSpan.codeUnitAt(4), PlaceholderSpan.placeholderCodeUnit);
+ expect(nestedSpan.codeUnitAt(5), isNull);
+ });
+}
diff --git a/packages/flutter/test/widgets/editable_text_cursor_test.dart b/packages/flutter/test/widgets/editable_text_cursor_test.dart
index 87ea54c..7b79f18 100644
--- a/packages/flutter/test/widgets/editable_text_cursor_test.dart
+++ b/packages/flutter/test/widgets/editable_text_cursor_test.dart
@@ -8,6 +8,8 @@
@TestOn('!chrome')
library;
+import 'dart:math' as math;
+
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
@@ -1105,4 +1107,137 @@
await tester.pump(const Duration(milliseconds: 500));
expect((findRenderEditable(tester).text! as TextSpan).text, '•••');
});
+
+ testWidgets('getLocalRectForCaret with empty text', (WidgetTester tester) async {
+ EditableText.debugDeterministicCursor = true;
+ addTearDown(() { EditableText.debugDeterministicCursor = false; });
+ const String text = '12';
+
+ final TextEditingController controller = TextEditingController.fromValue(
+ const TextEditingValue(
+ text: text,
+ selection: TextSelection.collapsed(offset: text.length),
+ ),
+ );
+
+ final Widget widget = EditableText(
+ autofocus: true,
+ backgroundCursorColor: Colors.grey,
+ controller: controller,
+ focusNode: FocusNode(),
+ style: const TextStyle(fontSize: 20),
+ textAlign: TextAlign.center,
+ keyboardType: TextInputType.text,
+ cursorColor: cursorColor,
+ maxLines: null,
+ );
+ await tester.pumpWidget(MaterialApp(home: widget));
+
+ final EditableTextState editableTextState = tester.firstState(find.byWidget(widget));
+ final RenderEditable renderEditable = editableTextState.renderEditable;
+ final Rect initialLocalCaretRect = renderEditable.getLocalRectForCaret(const TextPosition(offset: text.length));
+
+ for (int i = 0; i < 3; i++) {
+ Actions.invoke(primaryFocus!.context!, const DeleteCharacterIntent(forward: false));
+ await tester.pump();
+ expect(controller.text.length, math.max(0, text.length - 1 - i));
+ final Rect localRect = renderEditable.getLocalRectForCaret(
+ TextPosition(offset: controller.text.length),
+ );
+
+ expect(localRect.size, initialLocalCaretRect.size);
+ expect(localRect.top, initialLocalCaretRect.top);
+ expect(localRect.left, lessThan(initialLocalCaretRect.left));
+ }
+
+ expect(controller.text, isEmpty);
+ });
+
+ testWidgets('Caret center space test', (WidgetTester tester) async {
+ EditableText.debugDeterministicCursor = true;
+ addTearDown(() { EditableText.debugDeterministicCursor = false; });
+ final String text = 'test${' ' * 1000}';
+
+ final Widget widget = EditableText(
+ autofocus: true,
+ backgroundCursorColor: Colors.grey,
+ controller: TextEditingController.fromValue(
+ TextEditingValue(
+ text: text,
+ selection: TextSelection.collapsed(offset: text.length, affinity: TextAffinity.upstream),
+ ),
+ ),
+ focusNode: FocusNode(),
+ style: const TextStyle(),
+ textAlign: TextAlign.center,
+ keyboardType: TextInputType.text,
+ cursorColor: cursorColor,
+ cursorWidth: 13.0,
+ cursorHeight: 17.0,
+ maxLines: null,
+ );
+ await tester.pumpWidget(MaterialApp(home: widget));
+
+ final EditableTextState editableTextState = tester.firstState(find.byWidget(widget));
+ final Rect editableTextRect = tester.getRect(find.byWidget(widget));
+ final RenderEditable renderEditable = editableTextState.renderEditable;
+ // The trailing whitespaces are not line break opportunities.
+ expect(renderEditable.getLineAtOffset(TextPosition(offset: text.length)).start, 0);
+
+ // The caretRect shouldn't be outside of the RenderEditable.
+ final Rect caretRect = Rect.fromLTWH(
+ editableTextRect.right - 13.0 - 1.0,
+ editableTextRect.top,
+ 13.0,
+ 17.0,
+ );
+ expect(
+ renderEditable,
+ paints..rect(color: cursorColor, rect: caretRect),
+ );
+ }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308
+
+ testWidgets('getLocalRectForCaret reports the real caret Rect', (WidgetTester tester) async {
+ EditableText.debugDeterministicCursor = true;
+ addTearDown(() { EditableText.debugDeterministicCursor = false; });
+ final String text = 'test${' ' * 50}\n'
+ '2nd line\n'
+ '\n';
+
+ final TextEditingController controller = TextEditingController.fromValue(TextEditingValue(
+ text: text,
+ selection: const TextSelection.collapsed(offset: 0),
+ ));
+
+ final Widget widget = EditableText(
+ autofocus: true,
+ backgroundCursorColor: Colors.grey,
+ controller: controller,
+ focusNode: FocusNode(),
+ style: const TextStyle(fontSize: 20),
+ textAlign: TextAlign.center,
+ keyboardType: TextInputType.text,
+ cursorColor: cursorColor,
+ maxLines: null,
+ );
+ await tester.pumpWidget(MaterialApp(home: widget));
+
+ final EditableTextState editableTextState = tester.firstState(find.byWidget(widget));
+ final Rect editableTextRect = tester.getRect(find.byWidget(widget));
+ final RenderEditable renderEditable = editableTextState.renderEditable;
+
+ final Iterable<TextPosition> positions = List<int>
+ .generate(text.length + 1, (int index) => index)
+ .expand((int i) => <TextPosition>[TextPosition(offset: i, affinity: TextAffinity.upstream), TextPosition(offset: i)]);
+ for (final TextPosition position in positions) {
+ controller.selection = TextSelection.fromPosition(position);
+ await tester.pump();
+
+ final Rect localRect = renderEditable.getLocalRectForCaret(position);
+ expect(
+ renderEditable,
+ paints..rect(color: cursorColor, rect: localRect.shift(editableTextRect.topLeft)),
+ );
+ }
+ }, variant: TargetPlatformVariant.all());
}