@@ -273,14 +273,32 @@ class _UntilTextBoundary extends TextBoundary {
273273/// This is used to cache and pass the computed metrics regarding the
274274/// caret's size and position. This is preferred due to the expensive
275275/// nature of the calculation.
276- class _CaretMetrics {
277- const _CaretMetrics ({required this .offset, this .fullHeight});
276+ ///
277+ // This should be a sealed class: A _CaretMetrics is either a _LineCaretMetrics
278+ // or an _EmptyLineCaretMetrics.
279+ @immutable
280+ abstract class _CaretMetrics { }
281+
282+ /// The _CaretMetrics for carets located in a non-empty line. Carets located in a
283+ /// non-empty line are associated with a glyph within the same line.
284+ class _LineCaretMetrics implements _CaretMetrics {
285+ const _LineCaretMetrics ({required this .offset, required this .writingDirection, required this .fullHeight});
278286 /// The offset of the top left corner of the caret from the top left
279287 /// corner of the paragraph.
280288 final Offset offset;
281-
289+ /// The writing direction of the glyph the _CaretMetrics is associated with.
290+ final TextDirection writingDirection;
282291 /// The full height of the glyph at the caret position.
283- final double ? fullHeight;
292+ final double fullHeight;
293+ }
294+
295+ /// The _CaretMetrics for carets located in an empty line (when the text is
296+ /// empty, or the caret is between two a newline characters).
297+ class _EmptyLineCaretMetrics implements _CaretMetrics {
298+ const _EmptyLineCaretMetrics ({ required this .lineVerticalOffset });
299+
300+ /// The y offset of the unoccupied line.
301+ final double lineVerticalOffset;
284302}
285303
286304/// An object that paints a [TextSpan] tree into a [Canvas] .
@@ -466,7 +484,6 @@ class TextPainter {
466484 _paragraph = null ;
467485 _lineMetricsCache = null ;
468486 _previousCaretPosition = null ;
469- _previousCaretPrototype = null ;
470487 }
471488
472489 /// The (potentially styled) text to paint.
@@ -935,7 +952,6 @@ class TextPainter {
935952 // A change in layout invalidates the cached caret and line metrics as well.
936953 _lineMetricsCache = null ;
937954 _previousCaretPosition = null ;
938- _previousCaretPrototype = null ;
939955 _layoutParagraph (minWidth, maxWidth);
940956 _inlinePlaceholderBoxes = _paragraph! .getBoxesForPlaceholders ();
941957 }
@@ -1027,9 +1043,9 @@ class TextPainter {
10271043 // Unicode value for a zero width joiner character.
10281044 static const int _zwjUtf16 = 0x200d ;
10291045
1030- // Get the Rect of the cursor (in logical pixels) based off the near edge
1031- // of the character upstream from the given string offset.
1032- Rect ? _getRectFromUpstream (int offset, Rect caretPrototype ) {
1046+ // Get the caret metrics (in logical pixels) based off the near edge of the
1047+ // character upstream from the given string offset.
1048+ _CaretMetrics ? _getMetricsFromUpstream (int offset) {
10331049 final int plainTextLength = plainText.length;
10341050 if (plainTextLength == 0 || offset > plainTextLength) {
10351051 return null ;
@@ -1067,21 +1083,15 @@ class TextPainter {
10671083 }
10681084 final TextBox box = boxes.first;
10691085
1070- if (prevCodeUnit == NEWLINE_CODE_UNIT ) {
1071- return Rect .fromLTRB (_emptyOffset.dx, box.bottom, _emptyOffset.dx, box.bottom + box.bottom - box.top);
1072- }
1073-
1074- final double caretEnd = box.end;
1075- final double dx = box.direction == TextDirection .rtl ? caretEnd - caretPrototype.width : caretEnd;
1076- return Rect .fromLTRB (clampDouble (dx, 0 , _paragraph! .width), box.top,
1077- clampDouble (dx, 0 , _paragraph! .width), box.bottom);
1086+ return prevCodeUnit == NEWLINE_CODE_UNIT
1087+ ? _EmptyLineCaretMetrics (lineVerticalOffset: box.bottom)
1088+ : _LineCaretMetrics (offset: Offset (box.end, box.top), writingDirection: box.direction, fullHeight: box.bottom - box.top);
10781089 }
10791090 return null ;
10801091 }
1081-
1082- // Get the Rect of the cursor (in logical pixels) based off the near edge
1083- // of the character downstream from the given string offset.
1084- Rect ? _getRectFromDownstream (int offset, Rect caretPrototype) {
1092+ // Get the caret metrics (in logical pixels) based off the near edge of the
1093+ // character downstream from the given string offset.
1094+ _CaretMetrics ? _getMetricsFromDownstream (int offset) {
10851095 final int plainTextLength = plainText.length;
10861096 if (plainTextLength == 0 || offset < 0 ) {
10871097 return null ;
@@ -1116,38 +1126,33 @@ class TextPainter {
11161126 continue ;
11171127 }
11181128 final TextBox box = boxes.last;
1119- final double caretStart = box.start;
1120- final double dx = box.direction == TextDirection .rtl ? caretStart - caretPrototype.width : caretStart;
1121- return Rect .fromLTRB (clampDouble (dx, 0 , _paragraph! .width), box.top, clampDouble (dx, 0 , _paragraph! .width), box.bottom);
1129+ return _LineCaretMetrics (offset: Offset (box.start, box.top), writingDirection: box.direction, fullHeight: box.bottom - box.top);
11221130 }
11231131 return null ;
11241132 }
11251133
1126- Offset get _emptyOffset {
1127- assert (_debugAssertTextLayoutIsValid); // implies textDirection is non-null
1134+ static double _computePaintOffsetFraction (TextAlign textAlign, TextDirection textDirection) {
11281135 switch (textAlign) {
11291136 case TextAlign .left:
1130- return Offset .zero ;
1137+ return 0.0 ;
11311138 case TextAlign .right:
1132- return Offset (width, 0.0 ) ;
1139+ return 1.0 ;
11331140 case TextAlign .center:
1134- return Offset (width / 2.0 , 0.0 );
1135- case TextAlign .justify:
1141+ return 0.5 ;
11361142 case TextAlign .start:
1137- assert (textDirection != null );
1138- switch (textDirection! ) {
1143+ case TextAlign .justify :
1144+ switch (textDirection) {
11391145 case TextDirection .rtl:
1140- return Offset (width, 0.0 ) ;
1146+ return 1.0 ;
11411147 case TextDirection .ltr:
1142- return Offset .zero ;
1148+ return 0.0 ;
11431149 }
11441150 case TextAlign .end:
1145- assert (textDirection != null );
1146- switch (textDirection! ) {
1151+ switch (textDirection) {
11471152 case TextDirection .rtl:
1148- return Offset .zero ;
1153+ return 0.0 ;
11491154 case TextDirection .ltr:
1150- return Offset (width, 0.0 ) ;
1155+ return 1.0 ;
11511156 }
11521157 }
11531158 }
@@ -1156,8 +1161,33 @@ class TextPainter {
11561161 ///
11571162 /// Valid only after [layout] has been called.
11581163 Offset getOffsetForCaret (TextPosition position, Rect caretPrototype) {
1159- _computeCaretMetrics (position, caretPrototype);
1160- return _caretMetrics.offset;
1164+ final _CaretMetrics caretMetrics = _computeCaretMetrics (position);
1165+
1166+ if (caretMetrics is _EmptyLineCaretMetrics ) {
1167+ final double paintOffsetAlignment = _computePaintOffsetFraction (textAlign, textDirection! );
1168+ // The full width is not (width - caretPrototype.width)
1169+ // because RenderEditable reserves cursor width on the right. Ideally this
1170+ // should be handled by RenderEditable instead.
1171+ final double dx = paintOffsetAlignment == 0 ? 0 : paintOffsetAlignment * width;
1172+ return Offset (dx, caretMetrics.lineVerticalOffset);
1173+ }
1174+
1175+ final Offset offset;
1176+ switch ((caretMetrics as _LineCaretMetrics ).writingDirection) {
1177+ case TextDirection .rtl:
1178+ offset = Offset (caretMetrics.offset.dx - caretPrototype.width, caretMetrics.offset.dy);
1179+ break ;
1180+ case TextDirection .ltr:
1181+ offset = caretMetrics.offset;
1182+ break ;
1183+ }
1184+ // If offset.dx is outside of the advertised content area, then the associated
1185+ // glyph cluster belongs to a trailing newline character. Ideally the behavior
1186+ // should be handled by higher-level implementations (for instance,
1187+ // RenderEditable reserves width for showing the caret, it's best to handle
1188+ // the clamping there).
1189+ final double adjustedDx = clampDouble (offset.dx, 0 , width);
1190+ return Offset (adjustedDx, offset.dy);
11611191 }
11621192
11631193 /// {@template flutter.painting.textPainter.getFullHeightForCaret}
@@ -1166,8 +1196,8 @@ class TextPainter {
11661196 ///
11671197 /// Valid only after [layout] has been called.
11681198 double ? getFullHeightForCaret (TextPosition position, Rect caretPrototype) {
1169- _computeCaretMetrics (position, caretPrototype );
1170- return _caretMetrics .fullHeight;
1199+ final _CaretMetrics caretMetrics = _computeCaretMetrics (position);
1200+ return caretMetrics is _LineCaretMetrics ? caretMetrics .fullHeight : null ;
11711201 }
11721202
11731203 // Cached caret metrics. This allows multiple invokes of [getOffsetForCaret] and
@@ -1179,35 +1209,29 @@ class TextPainter {
11791209 // computed with. When new values are passed in, we recompute the caret metrics.
11801210 // only as necessary.
11811211 TextPosition ? _previousCaretPosition;
1182- Rect ? _previousCaretPrototype;
11831212
11841213 // Checks if the [position] and [caretPrototype] have changed from the cached
11851214 // version and recomputes the metrics required to position the caret.
1186- void _computeCaretMetrics (TextPosition position, Rect caretPrototype ) {
1215+ _CaretMetrics _computeCaretMetrics (TextPosition position) {
11871216 assert (_debugAssertTextLayoutIsValid);
1188- if (position == _previousCaretPosition && caretPrototype == _previousCaretPrototype ) {
1189- return ;
1217+ if (position == _previousCaretPosition) {
1218+ return _caretMetrics ;
11901219 }
11911220 final int offset = position.offset;
1192- Rect ? rect ;
1221+ final _CaretMetrics ? metrics ;
11931222 switch (position.affinity) {
11941223 case TextAffinity .upstream: {
1195- rect = _getRectFromUpstream (offset, caretPrototype ) ?? _getRectFromDownstream (offset, caretPrototype );
1224+ metrics = _getMetricsFromUpstream (offset) ?? _getMetricsFromDownstream (offset);
11961225 break ;
11971226 }
11981227 case TextAffinity .downstream: {
1199- rect = _getRectFromDownstream (offset, caretPrototype ) ?? _getRectFromUpstream (offset, caretPrototype );
1228+ metrics = _getMetricsFromDownstream (offset) ?? _getMetricsFromUpstream (offset);
12001229 break ;
12011230 }
12021231 }
1203- _caretMetrics = _CaretMetrics (
1204- offset: rect != null ? Offset (rect.left, rect.top) : _emptyOffset,
1205- fullHeight: rect != null ? rect.bottom - rect.top : null ,
1206- );
1207-
12081232 // Cache the input parameters to prevent repeat work later.
12091233 _previousCaretPosition = position;
1210- _previousCaretPrototype = caretPrototype ;
1234+ return _caretMetrics = metrics ?? const _EmptyLineCaretMetrics (lineVerticalOffset : 0 ) ;
12111235 }
12121236
12131237 /// Returns a list of rects that bound the given selection.
0 commit comments