Unverified Commit a0a854a7 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Relands "Changing `TextPainter.getOffsetForCaret` implementation to remove the...

Relands "Changing `TextPainter.getOffsetForCaret` implementation to remove the logarithmic search (#143281)" (reverted in #143801) (#143954)

The original PR was reverted because the new caret positioning callpath triggered a skparagraph assert. The assert has been removed. Relanding the PR with no changes applied.
parent c84565a6
...@@ -46,8 +46,9 @@ void main() { ...@@ -46,8 +46,9 @@ void main() {
const Duration durationBetweenActions = Duration(milliseconds: 20); const Duration durationBetweenActions = Duration(milliseconds: 20);
const String defaultText = 'I am a magnifier, fear me!'; const String defaultText = 'I am a magnifier, fear me!';
Future<void> showMagnifier(WidgetTester tester, String characterToTapOn) async { Future<void> showMagnifier(WidgetTester tester, int textOffset) async {
final Offset tapOffset = _textOffsetToPosition(tester, defaultText.indexOf(characterToTapOn)); assert(textOffset >= 0);
final Offset tapOffset = _textOffsetToPosition(tester, textOffset);
// Double tap 'Magnifier' word to show the selection handles. // Double tap 'Magnifier' word to show the selection handles.
final TestGesture testGesture = await tester.startGesture(tapOffset); final TestGesture testGesture = await tester.startGesture(tapOffset);
...@@ -59,11 +60,11 @@ void main() { ...@@ -59,11 +60,11 @@ void main() {
await testGesture.up(); await testGesture.up();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
final TextSelection selection = tester final TextEditingController controller = tester
.firstWidget<TextField>(find.byType(TextField)) .firstWidget<TextField>(find.byType(TextField))
.controller! .controller!;
.selection;
final TextSelection selection = controller.selection;
final RenderEditable renderEditable = _findRenderEditable(tester); final RenderEditable renderEditable = _findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = _globalize( final List<TextSelectionPoint> endpoints = _globalize(
renderEditable.getEndpointsForSelection(selection), renderEditable.getEndpointsForSelection(selection),
...@@ -86,7 +87,7 @@ void main() { ...@@ -86,7 +87,7 @@ void main() {
testWidgets('should show custom magnifier on drag', (WidgetTester tester) async { testWidgets('should show custom magnifier on drag', (WidgetTester tester) async {
await tester.pumpWidget(const example.TextMagnifierExampleApp(text: defaultText)); await tester.pumpWidget(const example.TextMagnifierExampleApp(text: defaultText));
await showMagnifier(tester, 'e'); await showMagnifier(tester, defaultText.indexOf('e'));
expect(find.byType(example.CustomMagnifier), findsOneWidget); expect(find.byType(example.CustomMagnifier), findsOneWidget);
await expectLater( await expectLater(
...@@ -96,16 +97,15 @@ void main() { ...@@ -96,16 +97,15 @@ void main() {
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }));
for (final TextDirection textDirection in TextDirection.values) { testWidgets('should show custom magnifier in RTL', (WidgetTester tester) async {
testWidgets('should show custom magnifier in $textDirection', (WidgetTester tester) async { const String text = 'أثارت زر';
final String text = textDirection == TextDirection.rtl ? 'أثارت زر' : defaultText; const String textToTapOn = 'ت';
final String textToTapOn = textDirection == TextDirection.rtl ? 'ت' : 'e';
await tester.pumpWidget(example.TextMagnifierExampleApp(textDirection: textDirection, text: text)); await tester.pumpWidget(const example.TextMagnifierExampleApp(textDirection: TextDirection.rtl, text: text));
await showMagnifier(tester, textToTapOn); await showMagnifier(tester, text.indexOf(textToTapOn));
expect(find.byType(example.CustomMagnifier), findsOneWidget); expect(find.byType(example.CustomMagnifier), findsOneWidget);
}); });
}
} }
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' show max, min; import 'dart:math' show max;
import 'dart:ui' as ui show import 'dart:ui' as ui show
BoxHeightStyle, BoxHeightStyle,
BoxWidthStyle, BoxWidthStyle,
...@@ -204,8 +204,14 @@ class WordBoundary extends TextBoundary { ...@@ -204,8 +204,14 @@ class WordBoundary extends TextBoundary {
} }
static bool _isNewline(int codePoint) { static bool _isNewline(int codePoint) {
// Carriage Return is not treated as a hard line break.
return switch (codePoint) { return switch (codePoint) {
0x000A || 0x0085 || 0x000B || 0x000C || 0x2028 || 0x2029 => true, 0x000A || // Line Feed
0x0085 || // New Line
0x000B || // Form Feed
0x000C || // Vertical Feed
0x2028 || // Line Separator
0x2029 => true, // Paragraph Separator
_ => false, _ => false,
}; };
} }
...@@ -270,7 +276,10 @@ class _UntilTextBoundary extends TextBoundary { ...@@ -270,7 +276,10 @@ class _UntilTextBoundary extends TextBoundary {
} }
class _TextLayout { class _TextLayout {
_TextLayout._(this._paragraph); _TextLayout._(this._paragraph, this.writingDirection, this.rawString);
final TextDirection writingDirection;
final String rawString;
// This field is not final because the owner TextPainter could create a new // This field is not final because the owner TextPainter could create a new
// ui.Paragraph with the exact same text layout (for example, when only the // ui.Paragraph with the exact same text layout (for example, when only the
...@@ -316,6 +325,57 @@ class _TextLayout { ...@@ -316,6 +325,57 @@ class _TextLayout {
}; };
} }
/// The line caret metrics representing the end of text location.
///
/// This is usually used when the caret is placed at the end of the text
/// (text.length, downstream), unless maxLines is set to a non-null value, in
/// which case the caret is placed at the visual end of the last visible line.
///
/// This should not be called when the paragraph is emtpy as the implementation
/// relies on line metrics.
///
/// When the last bidi level run in the paragraph and the parargraph's bidi
/// levels have opposite parities (which implies opposite writing directions),
/// this makes sure the caret is placed at the same "end" of the line as if the
/// line ended with a line feed.
late final _LineCaretMetrics _endOfTextCaretMetrics = _computeEndOfTextCaretAnchorOffset();
_LineCaretMetrics _computeEndOfTextCaretAnchorOffset() {
final int lastLineIndex = _paragraph.numberOfLines - 1;
assert(lastLineIndex >= 0);
final ui.LineMetrics lineMetrics = _paragraph.getLineMetricsAt(lastLineIndex)!;
// SkParagraph currently treats " " and "\t" as white spaces. Trailing white
// spaces don't contribute to the line width and thus require special handling
// when they're present.
// Luckily they have the same bidi embedding level as the paragraph as per
// https://unicode.org/reports/tr9/#L1, so we can anchor the caret to the
// last logical trailing space.
final bool hasTrailingSpaces = switch (rawString.codeUnitAt(rawString.length - 1)) {
0x9 || // horizontal tab
0x20 => true, // space
_ => false,
};
final double baseline = lineMetrics.baseline;
final double dx;
late final ui.GlyphInfo? lastGlyph = _paragraph.getGlyphInfoAt(rawString.length - 1);
// TODO(LongCatIsLooong): handle the case where maxLine is set to non-null
// and the last line ends with trailing whitespaces.
if (hasTrailingSpaces && lastGlyph != null) {
final Rect glyphBounds = lastGlyph.graphemeClusterLayoutBounds;
assert(!glyphBounds.isEmpty);
dx = switch (writingDirection) {
TextDirection.ltr => glyphBounds.right,
TextDirection.rtl => glyphBounds.left,
};
} else {
dx = switch (writingDirection) {
TextDirection.ltr => lineMetrics.left + lineMetrics.width,
TextDirection.rtl => lineMetrics.left,
};
}
return _LineCaretMetrics(offset: Offset(dx, baseline), writingDirection: writingDirection);
}
double _contentWidthFor(double minWidth, double maxWidth, TextWidthBasis widthBasis) { double _contentWidthFor(double minWidth, double maxWidth, TextWidthBasis widthBasis) {
return switch (widthBasis) { return switch (widthBasis) {
TextWidthBasis.longestLine => clampDouble(longestLine, minWidth, maxWidth), TextWidthBasis.longestLine => clampDouble(longestLine, minWidth, maxWidth),
...@@ -420,39 +480,29 @@ class _TextPainterLayoutCacheWithOffset { ...@@ -420,39 +480,29 @@ class _TextPainterLayoutCacheWithOffset {
List<ui.LineMetrics> get lineMetrics => _cachedLineMetrics ??= paragraph.computeLineMetrics(); List<ui.LineMetrics> get lineMetrics => _cachedLineMetrics ??= paragraph.computeLineMetrics();
List<ui.LineMetrics>? _cachedLineMetrics; List<ui.LineMetrics>? _cachedLineMetrics;
// Holds the TextPosition the last caret metrics were computed with. When new // Used to determine whether the caret metrics cache should be invalidated.
// values are passed in, we recompute the caret metrics only as necessary. int? _previousCaretPositionKey;
TextPosition? _previousCaretPosition;
} }
/// This is used to cache and pass the computed metrics regarding the /// The _CaretMetrics for carets located in a non-empty paragraph. Such carets
/// caret's size and position. This is preferred due to the expensive /// are anchored to the trailing edge or the leading edge of a glyph, or a
/// nature of the calculation. /// ligature component.
/// final class _LineCaretMetrics {
// A _CaretMetrics is either a _LineCaretMetrics or an _EmptyLineCaretMetrics. const _LineCaretMetrics({required this.offset, required this.writingDirection});
@immutable /// The offset from the top left corner of the paragraph to the caret's top
sealed class _CaretMetrics { } /// start location.
/// 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.
final 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; 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;
}
/// The _CaretMetrics for carets located in an empty line (when the text is /// The writing direction of the glyph the _LineCaretMetrics is associated with.
/// empty, or the caret is between two a newline characters). /// The value determines whether the cursor is painted to the left or to the
final class _EmptyLineCaretMetrics implements _CaretMetrics { /// right of [offset].
const _EmptyLineCaretMetrics({ required this.lineVerticalOffset }); final TextDirection writingDirection;
/// The y offset of the unoccupied line. _LineCaretMetrics shift(Offset offset) {
final double lineVerticalOffset; return offset == Offset.zero
? this
: _LineCaretMetrics(offset: offset + this.offset, writingDirection: writingDirection);
}
} }
const String _flutterPaintingLibrary = 'package:flutter/painting.dart'; const String _flutterPaintingLibrary = 'package:flutter/painting.dart';
...@@ -971,10 +1021,8 @@ class TextPainter { ...@@ -971,10 +1021,8 @@ class TextPainter {
} }
List<PlaceholderDimensions>? _placeholderDimensions; List<PlaceholderDimensions>? _placeholderDimensions;
ui.ParagraphStyle _createParagraphStyle([ TextDirection? defaultTextDirection ]) { ui.ParagraphStyle _createParagraphStyle([ TextAlign? textAlignOverride ]) {
// The defaultTextDirection argument is used for preferredLineHeight in case assert(textDirection != null, 'TextPainter.textDirection must be set to a non-null value before using the TextPainter.');
// textDirection hasn't yet been set.
assert(textDirection != null || defaultTextDirection != null, 'TextPainter.textDirection must be set to a non-null value before using the TextPainter.');
final TextStyle baseStyle = _text?.style ?? const TextStyle(); final TextStyle baseStyle = _text?.style ?? const TextStyle();
final StrutStyle? strutStyle = _strutStyle; final StrutStyle? strutStyle = _strutStyle;
...@@ -996,8 +1044,8 @@ class TextPainter { ...@@ -996,8 +1044,8 @@ class TextPainter {
); );
return baseStyle.getParagraphStyle( return baseStyle.getParagraphStyle(
textAlign: textAlign, textAlign: textAlignOverride ?? textAlign,
textDirection: textDirection ?? defaultTextDirection, textDirection: textDirection,
textScaler: textScaler, textScaler: textScaler,
maxLines: _maxLines, maxLines: _maxLines,
textHeightBehavior: _textHeightBehavior, textHeightBehavior: _textHeightBehavior,
...@@ -1010,7 +1058,7 @@ class TextPainter { ...@@ -1010,7 +1058,7 @@ class TextPainter {
ui.Paragraph? _layoutTemplate; ui.Paragraph? _layoutTemplate;
ui.Paragraph _createLayoutTemplate() { ui.Paragraph _createLayoutTemplate() {
final ui.ParagraphBuilder builder = ui.ParagraphBuilder( final ui.ParagraphBuilder builder = ui.ParagraphBuilder(
_createParagraphStyle(TextDirection.rtl), _createParagraphStyle(TextAlign.left),
); // direction doesn't matter, text is just a space ); // direction doesn't matter, text is just a space
final ui.TextStyle? textStyle = text?.style?.getTextStyle(textScaler: textScaler); final ui.TextStyle? textStyle = text?.style?.getTextStyle(textScaler: textScaler);
if (textStyle != null) { if (textStyle != null) {
...@@ -1021,6 +1069,7 @@ class TextPainter { ...@@ -1021,6 +1069,7 @@ class TextPainter {
..layout(const ui.ParagraphConstraints(width: double.infinity)); ..layout(const ui.ParagraphConstraints(width: double.infinity));
} }
ui.Paragraph _getOrCreateLayoutTemplate() => _layoutTemplate ??= _createLayoutTemplate();
/// The height of a space in [text] in logical pixels. /// The height of a space in [text] in logical pixels.
/// ///
/// Not every line of text in [text] will have this height, but this height /// Not every line of text in [text] will have this height, but this height
...@@ -1033,7 +1082,7 @@ class TextPainter { ...@@ -1033,7 +1082,7 @@ class TextPainter {
/// that contribute to the [preferredLineHeight]. If [text] is null or if it /// that contribute to the [preferredLineHeight]. If [text] is null or if it
/// specifies no styles, the default [TextStyle] values are used (a 10 pixel /// specifies no styles, the default [TextStyle] values are used (a 10 pixel
/// sans-serif font). /// sans-serif font).
double get preferredLineHeight => (_layoutTemplate ??= _createLayoutTemplate()).height; double get preferredLineHeight => _getOrCreateLayoutTemplate().height;
/// The width at which decreasing the width of the text would prevent it from /// The width at which decreasing the width of the text would prevent it from
/// painting itself completely within its bounds. /// painting itself completely within its bounds.
...@@ -1164,7 +1213,7 @@ class TextPainter { ...@@ -1164,7 +1213,7 @@ class TextPainter {
// called. // called.
final ui.Paragraph paragraph = (cachedLayout?.paragraph ?? _createParagraph(text)) final ui.Paragraph paragraph = (cachedLayout?.paragraph ?? _createParagraph(text))
..layout(ui.ParagraphConstraints(width: layoutMaxWidth)); ..layout(ui.ParagraphConstraints(width: layoutMaxWidth));
final _TextLayout layout = _TextLayout._(paragraph); final _TextLayout layout = _TextLayout._(paragraph, textDirection, plainText);
final double contentWidth = layout._contentWidthFor(minWidth, maxWidth, textWidthBasis); final double contentWidth = layout._contentWidthFor(minWidth, maxWidth, textWidthBasis);
final _TextPainterLayoutCacheWithOffset newLayoutCache; final _TextPainterLayoutCacheWithOffset newLayoutCache;
...@@ -1259,14 +1308,6 @@ class TextPainter { ...@@ -1259,14 +1308,6 @@ class TextPainter {
return value & 0xFC00 == 0xDC00; return value & 0xFC00 == 0xDC00;
} }
// Checks if the glyph is either [Unicode.RLM] or [Unicode.LRM]. These values take
// up zero space and do not have valid bounding boxes around them.
//
// We do not directly use the [Unicode] constants since they are strings.
static bool _isUnicodeDirectionality(int value) {
return value == 0x200F || value == 0x200E;
}
/// Returns the closest offset after `offset` at which the input cursor can be /// Returns the closest offset after `offset` at which the input cursor can be
/// positioned. /// positioned.
int? getOffsetAfter(int offset) { int? getOffsetAfter(int offset) {
...@@ -1289,118 +1330,15 @@ class TextPainter { ...@@ -1289,118 +1330,15 @@ class TextPainter {
return isLowSurrogate(prevCodeUnit) ? offset - 2 : offset - 1; return isLowSurrogate(prevCodeUnit) ? offset - 2 : offset - 1;
} }
// Unicode value for a zero width joiner character. // Get the caret metrics (in logical pixels) based off the trailing edge of the
static const int _zwjUtf16 = 0x200d;
// Get the caret metrics (in logical pixels) based off the near edge of the
// character upstream from the given string offset. // character upstream from the given string offset.
_CaretMetrics? _getMetricsFromUpstream(int offset) {
assert(offset >= 0);
final int plainTextLength = plainText.length;
if (plainTextLength == 0 || offset > plainTextLength) {
return null;
}
final int prevCodeUnit = plainText.codeUnitAt(max(0, offset - 1));
// If the upstream character is a newline, cursor is at start of next line
const int NEWLINE_CODE_UNIT = 10;
// Check for multi-code-unit glyphs such as emojis or zero width joiner.
final bool needsSearch = isHighSurrogate(prevCodeUnit) || isLowSurrogate(prevCodeUnit) || _text!.codeUnitAt(offset) == _zwjUtf16 || _isUnicodeDirectionality(prevCodeUnit);
int graphemeClusterLength = needsSearch ? 2 : 1;
List<TextBox> boxes = <TextBox>[];
while (boxes.isEmpty) {
final int prevRuneOffset = offset - graphemeClusterLength;
// Use BoxHeightStyle.strut to ensure that the caret's height fits within
// the line's height and is consistent throughout the line.
boxes = _layoutCache!.paragraph.getBoxesForRange(max(0, prevRuneOffset), offset, boxHeightStyle: ui.BoxHeightStyle.strut);
// When the range does not include a full cluster, no boxes will be returned.
if (boxes.isEmpty) {
// When we are at the beginning of the line, a non-surrogate position will
// return empty boxes. We break and try from downstream instead.
if (!needsSearch && prevCodeUnit == NEWLINE_CODE_UNIT) {
break; // Only perform one iteration if no search is required.
}
if (prevRuneOffset < -plainTextLength) {
break; // Stop iterating when beyond the max length of the text.
}
// Multiply by two to log(n) time cover the entire text span. This allows
// faster discovery of very long clusters and reduces the possibility
// of certain large clusters taking much longer than others, which can
// cause jank.
graphemeClusterLength *= 2;
continue;
}
// Try to identify the box nearest the offset. This logic works when
// there's just one box, and when all boxes have the same direction.
// It may not work in bidi text: https://github.com/flutter/flutter/issues/123424
final TextBox box = boxes.last.direction == TextDirection.ltr
? boxes.last : boxes.first;
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 caret metrics (in logical pixels) based off the near edge of the
// character downstream from the given string offset.
_CaretMetrics? _getMetricsFromDownstream(int offset) {
assert(offset >= 0);
final int plainTextLength = plainText.length;
if (plainTextLength == 0) {
return null;
}
// We cap the offset at the final index of plain text.
final int nextCodeUnit = plainText.codeUnitAt(min(offset, plainTextLength - 1));
// Check for multi-code-unit glyphs such as emojis or zero width joiner
final bool needsSearch = isHighSurrogate(nextCodeUnit) || isLowSurrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16 || _isUnicodeDirectionality(nextCodeUnit);
int graphemeClusterLength = needsSearch ? 2 : 1;
List<TextBox> boxes = <TextBox>[];
while (boxes.isEmpty) {
final int nextRuneOffset = offset + graphemeClusterLength;
// Use BoxHeightStyle.strut to ensure that the caret's height fits within
// the line's height and is consistent throughout the line.
boxes = _layoutCache!.paragraph.getBoxesForRange(offset, nextRuneOffset, boxHeightStyle: ui.BoxHeightStyle.strut);
// When the range does not include a full cluster, no boxes will be returned.
if (boxes.isEmpty) {
// When we are at the end of the line, a non-surrogate position will
// return empty boxes. We break and try from upstream instead.
if (!needsSearch) {
break; // Only perform one iteration if no search is required.
}
if (nextRuneOffset >= plainTextLength << 1) {
break; // Stop iterating when beyond the max length of the text.
}
// Multiply by two to log(n) time cover the entire text span. This allows
// faster discovery of very long clusters and reduces the possibility
// of certain large clusters taking much longer than others, which can
// cause jank.
graphemeClusterLength *= 2;
continue;
}
// Try to identify the box nearest the offset. This logic works when
// there's just one box, and when all boxes have the same direction.
// It may not work in bidi text: https://github.com/flutter/flutter/issues/123424
final TextBox box = boxes.first.direction == TextDirection.ltr
? boxes.first : boxes.last;
return _LineCaretMetrics(offset: Offset(box.start, box.top), writingDirection: box.direction, fullHeight: box.bottom - box.top);
}
return null;
}
static double _computePaintOffsetFraction(TextAlign textAlign, TextDirection textDirection) { static double _computePaintOffsetFraction(TextAlign textAlign, TextDirection textDirection) {
return switch ((textAlign, textDirection)) { return switch ((textAlign, textDirection)) {
(TextAlign.left, _) => 0.0, (TextAlign.left, _) => 0.0,
(TextAlign.right, _) => 1.0, (TextAlign.right, _) => 1.0,
(TextAlign.center, _) => 0.5, (TextAlign.center, _) => 0.5,
(TextAlign.start, TextDirection.ltr) => 0.0, (TextAlign.start || TextAlign.justify, TextDirection.ltr) => 0.0,
(TextAlign.start, TextDirection.rtl) => 1.0, (TextAlign.start || TextAlign.justify, TextDirection.rtl) => 1.0,
(TextAlign.justify, TextDirection.ltr) => 0.0,
(TextAlign.justify, TextDirection.rtl) => 1.0,
(TextAlign.end, TextDirection.ltr) => 1.0, (TextAlign.end, TextDirection.ltr) => 1.0,
(TextAlign.end, TextDirection.rtl) => 0.0, (TextAlign.end, TextDirection.rtl) => 0.0,
}; };
...@@ -1410,31 +1348,24 @@ class TextPainter { ...@@ -1410,31 +1348,24 @@ class TextPainter {
/// ///
/// Valid only after [layout] has been called. /// Valid only after [layout] has been called.
Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) { Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
final _CaretMetrics caretMetrics;
final _TextPainterLayoutCacheWithOffset layoutCache = _layoutCache!; final _TextPainterLayoutCacheWithOffset layoutCache = _layoutCache!;
if (position.offset < 0) { final _LineCaretMetrics? caretMetrics = _computeCaretMetrics(position);
// TODO(LongCatIsLooong): make this case impossible; see https://github.com/flutter/flutter/issues/79495
caretMetrics = const _EmptyLineCaretMetrics(lineVerticalOffset: 0);
} else {
caretMetrics = _computeCaretMetrics(position);
}
final Offset rawOffset; if (caretMetrics == null) {
switch (caretMetrics) {
case _EmptyLineCaretMetrics(:final double lineVerticalOffset):
final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection!); final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection!);
// The full width is not (width - caretPrototype.width) // The full width is not (width - caretPrototype.width), because
// because RenderEditable reserves cursor width on the right. Ideally this // RenderEditable reserves cursor width on the right. Ideally this
// should be handled by RenderEditable instead. // should be handled by RenderEditable instead.
final double dx = paintOffsetAlignment == 0 ? 0 : paintOffsetAlignment * layoutCache.contentWidth; final double dx = paintOffsetAlignment == 0 ? 0 : paintOffsetAlignment * layoutCache.contentWidth;
return Offset(dx, lineVerticalOffset); return Offset(dx, 0.0);
case _LineCaretMetrics(writingDirection: TextDirection.ltr, :final Offset offset):
rawOffset = offset;
case _LineCaretMetrics(writingDirection: TextDirection.rtl, :final Offset offset):
rawOffset = Offset(offset.dx - caretPrototype.width, offset.dy);
} }
final Offset rawOffset = switch (caretMetrics) {
_LineCaretMetrics(writingDirection: TextDirection.ltr, :final Offset offset) => offset,
_LineCaretMetrics(writingDirection: TextDirection.rtl, :final Offset offset) => Offset(offset.dx - caretPrototype.width, offset.dy),
};
// If offset.dx is outside of the advertised content area, then the associated // If offset.dx is outside of the advertised content area, then the associated
// glyph cluster belongs to a trailing newline character. Ideally the behavior // glyph belongs to a trailing whitespace character. Ideally the behavior
// should be handled by higher-level implementations (for instance, // should be handled by higher-level implementations (for instance,
// RenderEditable reserves width for showing the caret, it's best to handle // RenderEditable reserves width for showing the caret, it's best to handle
// the clamping there). // the clamping there).
...@@ -1448,38 +1379,136 @@ class TextPainter { ...@@ -1448,38 +1379,136 @@ class TextPainter {
/// ///
/// Valid only after [layout] has been called. /// Valid only after [layout] has been called.
double? getFullHeightForCaret(TextPosition position, Rect caretPrototype) { double? getFullHeightForCaret(TextPosition position, Rect caretPrototype) {
if (position.offset < 0) { final TextBox textBox = _getOrCreateLayoutTemplate().getBoxesForRange(0, 1, boxHeightStyle: ui.BoxHeightStyle.strut).single;
// TODO(LongCatIsLooong): make this case impossible; see https://github.com/flutter/flutter/issues/79495 return textBox.toRect().height;
return null;
} }
return switch (_computeCaretMetrics(position)) { bool _isNewlineAtOffset(int offset) => 0 <= offset && offset < plainText.length
_LineCaretMetrics(:final double fullHeight) => fullHeight, && WordBoundary._isNewline(plainText.codeUnitAt(offset));
_EmptyLineCaretMetrics() => null,
};
}
// Cached caret metrics. This allows multiple invokes of [getOffsetForCaret] and // Cached caret metrics. This allows multiple invokes of [getOffsetForCaret] and
// [getFullHeightForCaret] in a row without performing redundant and expensive // [getFullHeightForCaret] in a row without performing redundant and expensive
// get rect calls to the paragraph. // get rect calls to the paragraph.
late _CaretMetrics _caretMetrics; //
// The cache implementation assumes there's only one cursor at any given time.
late _LineCaretMetrics _caretMetrics;
// Checks if the [position] and [caretPrototype] have changed from the cached // This function returns the caret's offset and height for the given
// version and recomputes the metrics required to position the caret. // `position` in the text, or null if the paragraph is empty.
_CaretMetrics _computeCaretMetrics(TextPosition position) { //
// For a TextPosition, typically when its TextAffinity is downstream, the
// corresponding I-beam caret is anchored to the leading edge of the character
// at `offset` in the text. When the TextAffinity is upstream, the I-beam is
// then anchored to the trailing edge of the preceding character, except for a
// few edge cases:
//
// 1. empty paragraph: this method returns null and the caller handles this
// case.
//
// 2. (textLength, downstream), the end-of-text caret when the text is not
// empty: it's placed next to the trailing edge of the last line of the
// text, in case the text and its last bidi run have different writing
// directions. See the `_computeEndOfTextCaretAnchorOffset` method for more
// details.
//
// 3. (0, upstream), which isn't a valid position, but it's not a conventional
// "invalid" caret location either (the offset isn't negative). For
// historical reasons, this is treated as (0, downstream).
//
// 4. (x, upstream) where x - 1 points to a line break character. The caret
// should be displayed at the beginning of the newline instead of at the
// end of the previous line. Converts the location to (x, downstream). The
// choice we makes in 5. allows us to still check (x - 1) in case x points
// to a multi-code-unit character.
//
// 5. (x, downstream || upstream), where x points to a multi-code-unit
// character. There's no perfect caret placement in this case. Here we chose
// to draw the caret at the location that makes the most sense when the
// user wants to backspace (which also means it's left-arrow-key-biased):
//
// * downstream: show the caret at the leading edge of the character only if
// x points to the start of the grapheme. Otherwise show the caret at the
// leading edge of the next logical character.
// * upstream: show the caret at the trailing edge of the previous character
// only if x points to the start of the grapheme. Otherwise place the
// caret at the trailing edge of the character.
_LineCaretMetrics? _computeCaretMetrics(TextPosition position) {
assert(_debugAssertTextLayoutIsValid); assert(_debugAssertTextLayoutIsValid);
assert(!_debugNeedsRelayout); assert(!_debugNeedsRelayout);
final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!; final _TextPainterLayoutCacheWithOffset cachedLayout = _layoutCache!;
if (position == cachedLayout._previousCaretPosition) { // If nothing is laid out, top start is the only reasonable place to place
// the cursor.
// The HTML renderer reports numberOfLines == 1 when the text is empty:
// https://github.com/flutter/flutter/issues/143331
if (cachedLayout.paragraph.numberOfLines < 1 || plainText.isEmpty) {
// TODO(LongCatIsLooong): assert when an invalid position is given.
return null;
}
final (int offset, bool anchorToLeadingEdge) = switch (position) {
TextPosition(offset: 0) => (0, true), // As a special case, always anchor to the leading edge of the first grapheme regardless of the affinity.
TextPosition(:final int offset, affinity: TextAffinity.downstream) => (offset, true),
TextPosition(:final int offset, affinity: TextAffinity.upstream) when _isNewlineAtOffset(offset - 1) => (offset, true),
TextPosition(:final int offset, affinity: TextAffinity.upstream) => (offset - 1, false)
};
final int caretPositionCacheKey = anchorToLeadingEdge ? offset : -offset - 1;
if (caretPositionCacheKey == cachedLayout._previousCaretPositionKey) {
return _caretMetrics; return _caretMetrics;
} }
final int offset = position.offset;
final _CaretMetrics? metrics = switch (position.affinity) { final ui.GlyphInfo? glyphInfo = cachedLayout.paragraph.getGlyphInfoAt(offset);
TextAffinity.upstream => _getMetricsFromUpstream(offset) ?? _getMetricsFromDownstream(offset),
TextAffinity.downstream => _getMetricsFromDownstream(offset) ?? _getMetricsFromUpstream(offset), if (glyphInfo == null) {
// If the glyph isn't laid out, then the position points to a character
// that is not laid out. Use the EOT caret.
// TODO(LongCatIsLooong): assert when an invalid position is given.
final ui.Paragraph template = _getOrCreateLayoutTemplate();
assert(template.numberOfLines == 1);
final double baselineOffset = template.getLineMetricsAt(0)!.baseline;
return cachedLayout.layout._endOfTextCaretMetrics.shift(Offset(0.0, -baselineOffset));
}
final TextRange graphemeRange = glyphInfo.graphemeClusterCodeUnitRange;
// Works around a SkParagraph bug (https://github.com/flutter/flutter/issues/120836#issuecomment-1937343854):
// placeholders with a size of (0, 0) always have a rect of Rect.zero and a
// range of (0, 0).
if (graphemeRange.isCollapsed) {
assert(graphemeRange.start == 0);
return _computeCaretMetrics(TextPosition(offset: offset + 1));
}
if (anchorToLeadingEdge && graphemeRange.start != offset) {
assert(graphemeRange.end > graphemeRange.start + 1);
// Addresses the case where `offset` points to a multi-code-unit grapheme
// that doesn't start at `offset`.
return _computeCaretMetrics(TextPosition(offset: graphemeRange.end));
}
final _LineCaretMetrics metrics;
final List<TextBox> boxes = cachedLayout.paragraph
.getBoxesForRange(graphemeRange.start, graphemeRange.end, boxHeightStyle: ui.BoxHeightStyle.strut);
if (boxes.isNotEmpty) {
final TextBox box = boxes.single;
metrics =_LineCaretMetrics(
offset: Offset(anchorToLeadingEdge ? box.start : box.end, box.top),
writingDirection: box.direction,
);
} else {
// Fall back to glyphInfo. This should only happen when using the HTML renderer.
assert(kIsWeb && !isCanvasKit);
final Rect graphemeBounds = glyphInfo.graphemeClusterLayoutBounds;
final double dx = switch (glyphInfo.writingDirection) {
TextDirection.ltr => anchorToLeadingEdge ? graphemeBounds.left : graphemeBounds.right,
TextDirection.rtl => anchorToLeadingEdge ? graphemeBounds.right : graphemeBounds.left,
}; };
// Cache the input parameters to prevent repeat work later. metrics = _LineCaretMetrics(
cachedLayout._previousCaretPosition = position; offset: Offset(dx, graphemeBounds.top),
return _caretMetrics = metrics ?? const _EmptyLineCaretMetrics(lineVerticalOffset: 0); writingDirection: glyphInfo.writingDirection,
);
}
cachedLayout._previousCaretPositionKey = caretPositionCacheKey;
return _caretMetrics = metrics;
} }
/// Returns a list of rects that bound the given selection. /// Returns a list of rects that bound the given selection.
......
...@@ -6941,7 +6941,7 @@ void main() { ...@@ -6941,7 +6941,7 @@ void main() {
// the arrow should not point exactly to the caret because the caret is // the arrow should not point exactly to the caret because the caret is
// too close to the right. // too close to the right.
controller.dispose(); controller.dispose();
controller = TextEditingController(text: List<String>.filled(200, 'a').join()); controller = TextEditingController(text: 'a' * 200);
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
...@@ -7002,7 +7002,7 @@ void main() { ...@@ -7002,7 +7002,7 @@ void main() {
// Normal centered collapsed selection. The toolbar arrow should point down, and // Normal centered collapsed selection. The toolbar arrow should point down, and
// it should point exactly to the caret. // it should point exactly to the caret.
controller.dispose(); controller.dispose();
controller = TextEditingController(text: List<String>.filled(200, 'a').join()); controller = TextEditingController(text: 'a' * 200);
addTearDown(controller.dispose); addTearDown(controller.dispose);
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
......
...@@ -15222,6 +15222,8 @@ void main() { ...@@ -15222,6 +15222,8 @@ void main() {
bool isWide = false; bool isWide = false;
const double wideWidth = 300.0; const double wideWidth = 300.0;
const double narrowWidth = 200.0; const double narrowWidth = 200.0;
const TextStyle style = TextStyle(fontSize: 10, height: 1.0, letterSpacing: 0.0, wordSpacing: 0.0);
const double caretWidth = 2.0;
final TextEditingController controller = _textEditingController(); final TextEditingController controller = _textEditingController();
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
...@@ -15234,6 +15236,7 @@ void main() { ...@@ -15234,6 +15236,7 @@ void main() {
key: textFieldKey, key: textFieldKey,
controller: controller, controller: controller,
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
style: style,
), ),
); );
}, },
...@@ -15250,15 +15253,17 @@ void main() { ...@@ -15250,15 +15253,17 @@ void main() {
expect(inputWidth, narrowWidth); expect(inputWidth, narrowWidth);
expect(cursorRight, inputWidth - kCaretGap); expect(cursorRight, inputWidth - kCaretGap);
// After entering some text, the cursor remains on the right of the input. const String text = '12345';
await tester.enterText(find.byType(TextField), '12345'); // After entering some text, the cursor is placed to the left of the text
// because the paragraph's writing direction is RTL.
await tester.enterText(find.byType(TextField), text);
await tester.pump(); await tester.pump();
editable = findRenderEditable(tester); editable = findRenderEditable(tester);
cursorRight = editable.getLocalRectForCaret( cursorRight = editable.getLocalRectForCaret(
TextPosition(offset: controller.value.text.length), TextPosition(offset: controller.value.text.length),
).topRight.dx; ).topRight.dx;
inputWidth = editable.size.width; inputWidth = editable.size.width;
expect(cursorRight, inputWidth - kCaretGap); expect(cursorRight, inputWidth - kCaretGap - text.length * 10 - caretWidth);
// Since increasing the width of the input moves its right edge further to // Since increasing the width of the input moves its right edge further to
// the right, the cursor has followed this change and still appears on the // the right, the cursor has followed this change and still appears on the
...@@ -15273,7 +15278,7 @@ void main() { ...@@ -15273,7 +15278,7 @@ void main() {
).topRight.dx; ).topRight.dx;
inputWidth = editable.size.width; inputWidth = editable.size.width;
expect(inputWidth, wideWidth); expect(inputWidth, wideWidth);
expect(cursorRight, inputWidth - kCaretGap); expect(cursorRight, inputWidth - kCaretGap - text.length * 10 - caretWidth);
}); });
testWidgets('Text selection menu hides after select all on desktop', (WidgetTester tester) async { testWidgets('Text selection menu hides after select all on desktop', (WidgetTester tester) async {
......
...@@ -301,9 +301,9 @@ void main() { ...@@ -301,9 +301,9 @@ void main() {
painter.getOffsetForCaret(const TextPosition(offset: 2, affinity: TextAffinity.upstream), Rect.zero), painter.getOffsetForCaret(const TextPosition(offset: 2, affinity: TextAffinity.upstream), Rect.zero),
const Offset(0.0, 10.0), const Offset(0.0, 10.0),
); );
expect( // after the Alef expect( // To the right of the Alef
painter.getOffsetForCaret(const TextPosition(offset: 2), Rect.zero), painter.getOffsetForCaret(const TextPosition(offset: 2), Rect.zero),
const Offset(0.0, 10.0), const Offset(10.0, 10.0),
); );
expect( expect(
......
...@@ -117,6 +117,7 @@ List<double> caretOffsetsForTextSpan(TextDirection textDirection, TextSpan text) ...@@ -117,6 +117,7 @@ List<double> caretOffsetsForTextSpan(TextDirection textDirection, TextSpan text)
} }
void main() { void main() {
group('caret', () {
test('TextPainter caret test', () { test('TextPainter caret test', () {
final TextPainter painter = TextPainter() final TextPainter painter = TextPainter()
..textDirection = TextDirection.ltr; ..textDirection = TextDirection.ltr;
...@@ -250,11 +251,11 @@ void main() { ...@@ -250,11 +251,11 @@ void main() {
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 19), ui.Rect.zero); caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 19), ui.Rect.zero);
expect(caretOffset.dx, 98); // 👏 expect(caretOffset.dx, 98); // 👏
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 20), ui.Rect.zero); caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 20), ui.Rect.zero);
expect(caretOffset.dx, 98); // 👏 expect(caretOffset.dx, 126); // 👏
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 21), ui.Rect.zero); caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 21), ui.Rect.zero);
expect(caretOffset.dx, 98); // <medium skin tone modifier> expect(caretOffset.dx, 126); // <medium skin tone modifier>
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 22), ui.Rect.zero); caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 22), ui.Rect.zero);
expect(caretOffset.dx, 98); // <medium skin tone modifier> expect(caretOffset.dx, 126); // <medium skin tone modifier>
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 23), ui.Rect.zero); caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 23), ui.Rect.zero);
expect(caretOffset.dx, 126); // end of string expect(caretOffset.dx, 126); // end of string
painter.dispose(); painter.dispose();
...@@ -323,7 +324,7 @@ void main() { ...@@ -323,7 +324,7 @@ void main() {
TextSpan(text: 'words ', style: TextStyle(fontWeight: FontWeight.bold)), TextSpan(text: 'words ', style: TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: '👩‍🚀', style: TextStyle()), TextSpan(text: '👩‍🚀', style: TextStyle()),
])), ])),
<double>[0, 14, 28, 42, 56, 70, 84, 84, 84, 84, 84, 112]); <double>[0, 14, 28, 42, 56, 70, 84, 112, 112, 112, 112, 112]);
}, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308 }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308
test('TextPainter caret emoji test RTL: letters next to emoji, as separate TextBoxes', () { test('TextPainter caret emoji test RTL: letters next to emoji, as separate TextBoxes', () {
...@@ -341,7 +342,7 @@ void main() { ...@@ -341,7 +342,7 @@ void main() {
TextSpan(text: 'מילים ', style: TextStyle(fontWeight: FontWeight.bold)), TextSpan(text: 'מילים ', style: TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: '👩‍🚀', style: TextStyle()), TextSpan(text: '👩‍🚀', style: TextStyle()),
])), ])),
<double>[112, 98, 84, 70, 56, 42, 28, 28, 28, 28, 28, 0]); <double>[112, 98, 84, 70, 56, 42, 28, 0, 0, 0, 0, 0]);
}, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308 }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308
test('TextPainter caret center space test', () { test('TextPainter caret center space test', () {
...@@ -367,181 +368,85 @@ void main() { ...@@ -367,181 +368,85 @@ void main() {
painter.dispose(); painter.dispose();
}, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308 }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308
test('TextPainter error test', () { test('TextPainter caret height and line height', () {
final TextPainter painter = TextPainter(textDirection: TextDirection.ltr); final TextPainter painter = TextPainter()
..textDirection = TextDirection.ltr
expect( ..strutStyle = const StrutStyle(fontSize: 50.0);
() => painter.paint(MockCanvas(), Offset.zero),
throwsA(isA<StateError>().having(
(StateError error) => error.message,
'message',
contains('TextPainter.paint called when text geometry was not yet calculated'),
)),
);
painter.dispose();
});
test('TextPainter requires textDirection', () {
final TextPainter painter1 = TextPainter(text: const TextSpan(text: ''));
expect(painter1.layout, throwsStateError);
final TextPainter painter2 = TextPainter(text: const TextSpan(text: ''), textDirection: TextDirection.rtl);
expect(painter2.layout, isNot(throwsStateError));
});
test('TextPainter size test', () {
final TextPainter painter = TextPainter(
text: const TextSpan(
text: 'X',
style: TextStyle(inherit: false, fontSize: 123.0),
),
textDirection: TextDirection.ltr,
);
painter.layout();
expect(painter.size, const Size(123.0, 123.0));
painter.dispose();
});
test('TextPainter textScaler test', () { const String text = 'A';
final TextPainter painter = TextPainter( painter.text = const TextSpan(text: text, style: TextStyle(height: 1.0));
text: const TextSpan(
text: 'X',
style: TextStyle(inherit: false, fontSize: 10.0),
),
textDirection: TextDirection.ltr,
textScaler: const TextScaler.linear(2.0),
);
painter.layout(); painter.layout();
expect(painter.size, const Size(20.0, 20.0));
painter.dispose();
});
test('TextPainter textScaler null style test', () { final double caretHeight = painter.getFullHeightForCaret(
final TextPainter painter = TextPainter( const ui.TextPosition(offset: 0),
text: const TextSpan( ui.Rect.zero,
text: 'X', )!;
), expect(caretHeight, 50.0);
textDirection: TextDirection.ltr,
textScaler: const TextScaler.linear(2.0),
);
painter.layout();
expect(painter.size, const Size(28.0, 28.0));
painter.dispose(); painter.dispose();
}); }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308
test('TextPainter default text height is 14 pixels', () { test('upstream downstream makes no difference in the same line within the same bidi run', () {
final TextPainter painter = TextPainter( final TextPainter painter = TextPainter(textDirection: TextDirection.ltr)
text: const TextSpan(text: 'x'), ..text = const TextSpan(text: 'aa')
textDirection: TextDirection.ltr, ..layout();
);
painter.layout();
expect(painter.preferredLineHeight, 14.0);
expect(painter.size, const Size(14.0, 14.0));
painter.dispose();
});
test('TextPainter sets paragraph size from root', () { final Rect largeRect = Offset.zero & const Size.square(5);
final TextPainter painter = TextPainter( expect(
text: const TextSpan(text: 'x', style: TextStyle(fontSize: 100.0)), painter.getOffsetForCaret(const TextPosition(offset: 1), largeRect),
textDirection: TextDirection.ltr, painter.getOffsetForCaret(const TextPosition(offset: 1, affinity: TextAffinity.upstream), largeRect),
); );
painter.layout();
expect(painter.preferredLineHeight, 100.0);
expect(painter.size, const Size(100.0, 100.0));
painter.dispose();
}); });
test('TextPainter intrinsic dimensions', () { test('trailing newlines', () {
const TextStyle style = TextStyle( const double fontSize = 14.0;
inherit: false, final TextPainter painter = TextPainter();
fontSize: 10.0, final Rect largeRect = Offset.zero & const Size.square(5);
); String text = 'a ';
TextPainter painter; painter
..text = TextSpan(text: text)
painter = TextPainter( ..textDirection = TextDirection.ltr
text: const TextSpan( ..layout(minWidth: 1000.0, maxWidth: 1000.0);
text: 'X X X', expect(
style: style, painter.getOffsetForCaret(TextPosition(offset: text.length), largeRect).dx,
), text.length * fontSize,
textDirection: TextDirection.ltr,
); );
painter.layout();
expect(painter.size, const Size(50.0, 10.0));
expect(painter.minIntrinsicWidth, 10.0);
expect(painter.maxIntrinsicWidth, 50.0);
painter.dispose();
painter = TextPainter( text = 'ل ';
text: const TextSpan( painter
text: 'X X X', ..text = TextSpan(text: text)
style: style, ..textDirection = TextDirection.rtl
), ..layout(minWidth: 1000.0, maxWidth: 1000.0);
textDirection: TextDirection.ltr, expect(
ellipsis: 'e', painter.getOffsetForCaret(TextPosition(offset: text.length), largeRect).dx,
1000 - text.length * fontSize - largeRect.width,
); );
painter.layout(); }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308
expect(painter.size, const Size(50.0, 10.0));
expect(painter.minIntrinsicWidth, 50.0);
expect(painter.maxIntrinsicWidth, 50.0);
painter.dispose();
painter = TextPainter( test('End of text caret when the text ends with +1 bidi level', () {
text: const TextSpan( const double fontSize = 14.0;
text: 'X X XXXX', final TextPainter painter = TextPainter();
style: style, final Rect largeRect = Offset.zero & const Size.square(5);
), const String text = 'aل';
textDirection: TextDirection.ltr, painter
maxLines: 2, ..text = const TextSpan(text: text)
); ..textDirection = TextDirection.ltr
painter.layout(); ..layout(minWidth: 1000.0, maxWidth: 1000.0);
expect(painter.size, const Size(80.0, 10.0));
expect(painter.minIntrinsicWidth, 40.0);
expect(painter.maxIntrinsicWidth, 80.0);
painter.dispose();
painter = TextPainter( expect(
text: const TextSpan( painter.getOffsetForCaret(const TextPosition(offset: 0), largeRect).dx,
text: 'X X XXXX XX', 0.0,
style: style,
),
textDirection: TextDirection.ltr,
maxLines: 2,
); );
painter.layout(); expect(
expect(painter.size, const Size(110.0, 10.0)); painter.getOffsetForCaret(const TextPosition(offset: 1), largeRect).dx,
expect(painter.minIntrinsicWidth, 70.0); fontSize * 2 - largeRect.width,
expect(painter.maxIntrinsicWidth, 110.0);
painter.dispose();
painter = TextPainter(
text: const TextSpan(
text: 'XXXXXXXX XXXX XX X',
style: style,
),
textDirection: TextDirection.ltr,
maxLines: 2,
); );
painter.layout(); expect(
expect(painter.size, const Size(180.0, 10.0)); painter.getOffsetForCaret(const TextPosition(offset: 2), largeRect).dx,
expect(painter.minIntrinsicWidth, 90.0); fontSize * 2,
expect(painter.maxIntrinsicWidth, 180.0);
painter.dispose();
painter = TextPainter(
text: const TextSpan(
text: 'X XX XXXX XXXXXXXX',
style: style,
),
textDirection: TextDirection.ltr,
maxLines: 2,
); );
painter.layout(); }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308
expect(painter.size, const Size(180.0, 10.0));
expect(painter.minIntrinsicWidth, 90.0);
expect(painter.maxIntrinsicWidth, 180.0);
painter.dispose();
}, skip: true); // https://github.com/flutter/flutter/issues/13512
test('TextPainter handles newlines properly', () { test('handles newlines properly', () {
final TextPainter painter = TextPainter() final TextPainter painter = TextPainter()
..textDirection = TextDirection.ltr; ..textDirection = TextDirection.ltr;
...@@ -558,53 +463,53 @@ void main() { ...@@ -558,53 +463,53 @@ void main() {
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001)); expect(caretOffset.dx, SIZE_OF_A * offset);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001)); expect(caretOffset.dx, SIZE_OF_A * offset);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
offset = 1; offset = 1;
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001)); expect(caretOffset.dx, SIZE_OF_A * offset);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001)); expect(caretOffset.dx, SIZE_OF_A * offset);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
offset = 2; offset = 2;
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001)); expect(caretOffset.dx, SIZE_OF_A * offset);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001)); expect(caretOffset.dx, SIZE_OF_A * offset);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
offset = 3; offset = 3;
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001)); expect(caretOffset.dx, SIZE_OF_A * offset);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001)); expect(caretOffset.dx, SIZE_OF_A * offset);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
// For explicit newlines, getOffsetForCaret places the caret at the location // For explicit newlines, getOffsetForCaret places the caret at the location
// indicated by offset regardless of affinity. // indicated by offset regardless of affinity.
...@@ -616,40 +521,40 @@ void main() { ...@@ -616,40 +521,40 @@ void main() {
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
offset = 1; offset = 1;
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A);
offset = 2; offset = 2;
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 2, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A * 2);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 2, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A * 2);
// getOffsetForCaret in an unwrapped string with explicit newlines is the // getOffsetForCaret in an unwrapped string with explicit newlines is the
// same for either affinity. // same for either affinity.
...@@ -661,27 +566,27 @@ void main() { ...@@ -661,27 +566,27 @@ void main() {
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
offset = 1; offset = 1;
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A);
// When text wraps on its own, getOffsetForCaret disambiguates between the // When text wraps on its own, getOffsetForCaret disambiguates between the
// end of one line and start of next using affinity. // end of one line and start of next using affinity.
...@@ -693,15 +598,15 @@ void main() { ...@@ -693,15 +598,15 @@ void main() {
ui.Rect.zero, ui.Rect.zero,
); );
// When affinity is downstream, cursor is at beginning of second line // When affinity is downstream, cursor is at beginning of second line
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: text.length - 1, affinity: ui.TextAffinity.upstream), ui.TextPosition(offset: text.length - 1, affinity: ui.TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
// When affinity is upstream, cursor is at end of first line // When affinity is upstream, cursor is at end of first line
expect(caretOffset.dx, moreOrLessEquals(98.0, epsilon: 0.0001)); expect(caretOffset.dx, 98.0);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
// When given a string with a newline at the end, getOffsetForCaret puts // When given a string with a newline at the end, getOffsetForCaret puts
// the cursor at the start of the next line regardless of affinity // the cursor at the start of the next line regardless of affinity
...@@ -712,15 +617,15 @@ void main() { ...@@ -712,15 +617,15 @@ void main() {
ui.TextPosition(offset: text.length), ui.TextPosition(offset: text.length),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A);
offset = text.length; offset = text.length;
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A);
// Given a one-line right aligned string, positioning the cursor at offset 0 // Given a one-line right aligned string, positioning the cursor at offset 0
// means that it appears at the "end" of the string, after the character // means that it appears at the "end" of the string, after the character
...@@ -734,8 +639,8 @@ void main() { ...@@ -734,8 +639,8 @@ void main() {
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
painter.textAlign = TextAlign.left; painter.textAlign = TextAlign.left;
// When given an offset after a newline in the middle of a string, // When given an offset after a newline in the middle of a string,
...@@ -749,14 +654,14 @@ void main() { ...@@ -749,14 +654,14 @@ void main() {
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A);
// When given a string with multiple trailing newlines, places the caret // When given a string with multiple trailing newlines, places the caret
// in the position given by offset regardless of affinity. // in the position given by offset regardless of affinity.
...@@ -766,14 +671,14 @@ void main() { ...@@ -766,14 +671,14 @@ void main() {
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * 3, epsilon: 0.0001)); expect(caretOffset.dx, SIZE_OF_A * 3);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * 3, epsilon: 0.0001)); expect(caretOffset.dx, SIZE_OF_A * 3);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
offset = 4; offset = 4;
painter.text = TextSpan(text: text); painter.text = TextSpan(text: text);
...@@ -782,43 +687,43 @@ void main() { ...@@ -782,43 +687,43 @@ void main() {
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.001)); expect(caretOffset.dy, SIZE_OF_A);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A);
offset = 5; offset = 5;
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 2, epsilon: 0.001)); expect(caretOffset.dy, SIZE_OF_A * 2);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 2, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A * 2);
offset = 6; offset = 6;
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 3, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A * 3);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 3, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A * 3);
// When given a string with multiple leading newlines, places the caret in // When given a string with multiple leading newlines, places the caret in
// the position given by offset regardless of affinity. // the position given by offset regardless of affinity.
...@@ -830,59 +735,234 @@ void main() { ...@@ -830,59 +735,234 @@ void main() {
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 3, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A * 3);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 3, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A * 3);
offset = 2; offset = 2;
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 2, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A * 2);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 2, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A * 2);
offset = 1; offset = 1;
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy,moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001)); expect(caretOffset.dy,SIZE_OF_A);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001)); expect(caretOffset.dy, SIZE_OF_A);
offset = 0; offset = 0;
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset), ui.TextPosition(offset: offset),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
caretOffset = painter.getOffsetForCaret( caretOffset = painter.getOffsetForCaret(
ui.TextPosition(offset: offset, affinity: TextAffinity.upstream), ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
ui.Rect.zero, ui.Rect.zero,
); );
expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dx, 0.0);
expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001)); expect(caretOffset.dy, 0.0);
painter.dispose();
});
});
test('TextPainter error test', () {
final TextPainter painter = TextPainter(textDirection: TextDirection.ltr);
expect(
() => painter.paint(MockCanvas(), Offset.zero),
throwsA(isA<StateError>().having(
(StateError error) => error.message,
'message',
contains('TextPainter.paint called when text geometry was not yet calculated'),
)),
);
painter.dispose();
});
test('TextPainter requires textDirection', () {
final TextPainter painter1 = TextPainter(text: const TextSpan(text: ''));
expect(painter1.layout, throwsStateError);
final TextPainter painter2 = TextPainter(text: const TextSpan(text: ''), textDirection: TextDirection.rtl);
expect(painter2.layout, isNot(throwsStateError));
});
test('TextPainter size test', () {
final TextPainter painter = TextPainter(
text: const TextSpan(
text: 'X',
style: TextStyle(inherit: false, fontSize: 123.0),
),
textDirection: TextDirection.ltr,
);
painter.layout();
expect(painter.size, const Size(123.0, 123.0));
painter.dispose();
});
test('TextPainter textScaler test', () {
final TextPainter painter = TextPainter(
text: const TextSpan(
text: 'X',
style: TextStyle(inherit: false, fontSize: 10.0),
),
textDirection: TextDirection.ltr,
textScaler: const TextScaler.linear(2.0),
);
painter.layout();
expect(painter.size, const Size(20.0, 20.0));
painter.dispose();
});
test('TextPainter textScaler null style test', () {
final TextPainter painter = TextPainter(
text: const TextSpan(
text: 'X',
),
textDirection: TextDirection.ltr,
textScaler: const TextScaler.linear(2.0),
);
painter.layout();
expect(painter.size, const Size(28.0, 28.0));
painter.dispose();
});
test('TextPainter default text height is 14 pixels', () {
final TextPainter painter = TextPainter(
text: const TextSpan(text: 'x'),
textDirection: TextDirection.ltr,
);
painter.layout();
expect(painter.preferredLineHeight, 14.0);
expect(painter.size, const Size(14.0, 14.0));
painter.dispose(); painter.dispose();
}); });
test('TextPainter sets paragraph size from root', () {
final TextPainter painter = TextPainter(
text: const TextSpan(text: 'x', style: TextStyle(fontSize: 100.0)),
textDirection: TextDirection.ltr,
);
painter.layout();
expect(painter.preferredLineHeight, 100.0);
expect(painter.size, const Size(100.0, 100.0));
painter.dispose();
});
test('TextPainter intrinsic dimensions', () {
const TextStyle style = TextStyle(
inherit: false,
fontSize: 10.0,
);
TextPainter painter;
painter = TextPainter(
text: const TextSpan(
text: 'X X X',
style: style,
),
textDirection: TextDirection.ltr,
);
painter.layout();
expect(painter.size, const Size(50.0, 10.0));
expect(painter.minIntrinsicWidth, 10.0);
expect(painter.maxIntrinsicWidth, 50.0);
painter.dispose();
painter = TextPainter(
text: const TextSpan(
text: 'X X X',
style: style,
),
textDirection: TextDirection.ltr,
ellipsis: 'e',
);
painter.layout();
expect(painter.size, const Size(50.0, 10.0));
expect(painter.minIntrinsicWidth, 50.0);
expect(painter.maxIntrinsicWidth, 50.0);
painter.dispose();
painter = TextPainter(
text: const TextSpan(
text: 'X X XXXX',
style: style,
),
textDirection: TextDirection.ltr,
maxLines: 2,
);
painter.layout();
expect(painter.size, const Size(80.0, 10.0));
expect(painter.minIntrinsicWidth, 40.0);
expect(painter.maxIntrinsicWidth, 80.0);
painter.dispose();
painter = TextPainter(
text: const TextSpan(
text: 'X X XXXX XX',
style: style,
),
textDirection: TextDirection.ltr,
maxLines: 2,
);
painter.layout();
expect(painter.size, const Size(110.0, 10.0));
expect(painter.minIntrinsicWidth, 70.0);
expect(painter.maxIntrinsicWidth, 110.0);
painter.dispose();
painter = TextPainter(
text: const TextSpan(
text: 'XXXXXXXX XXXX XX X',
style: style,
),
textDirection: TextDirection.ltr,
maxLines: 2,
);
painter.layout();
expect(painter.size, const Size(180.0, 10.0));
expect(painter.minIntrinsicWidth, 90.0);
expect(painter.maxIntrinsicWidth, 180.0);
painter.dispose();
painter = TextPainter(
text: const TextSpan(
text: 'X XX XXXX XXXXXXXX',
style: style,
),
textDirection: TextDirection.ltr,
maxLines: 2,
);
painter.layout();
expect(painter.size, const Size(180.0, 10.0));
expect(painter.minIntrinsicWidth, 90.0);
expect(painter.maxIntrinsicWidth, 180.0);
painter.dispose();
}, skip: true); // https://github.com/flutter/flutter/issues/13512
test('TextPainter widget span', () { test('TextPainter widget span', () {
final TextPainter painter = TextPainter() final TextPainter painter = TextPainter()
..textDirection = TextDirection.ltr; ..textDirection = TextDirection.ltr;
...@@ -1053,23 +1133,6 @@ void main() { ...@@ -1053,23 +1133,6 @@ void main() {
painter.dispose(); painter.dispose();
}, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/122066 }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/122066
test('TextPainter caret height and line height', () {
final TextPainter painter = TextPainter()
..textDirection = TextDirection.ltr
..strutStyle = const StrutStyle(fontSize: 50.0);
const String text = 'A';
painter.text = const TextSpan(text: text, style: TextStyle(height: 1.0));
painter.layout();
final double caretHeight = painter.getFullHeightForCaret(
const ui.TextPosition(offset: 0),
ui.Rect.zero,
)!;
expect(caretHeight, 50.0);
painter.dispose();
}, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308
group('TextPainter line-height', () { group('TextPainter line-height', () {
test('half-leading', () { test('half-leading', () {
const TextStyle style = TextStyle( const TextStyle style = TextStyle(
......
...@@ -976,7 +976,7 @@ void main() { ...@@ -976,7 +976,7 @@ void main() {
expect(find.byType(EditableText), paints expect(find.byType(EditableText), paints
..rrect( ..rrect(
rrect: RRect.fromRectAndRadius( rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(193.83334350585938, -0.916666666666668, 196.83334350585938, 19.083333969116211), const Rect.fromLTWH(193.83334350585938, -0.916666666666668, 3.0, 20.0),
const Radius.circular(1.0), const Radius.circular(1.0),
), ),
color: const Color(0xbf2196f3), color: const Color(0xbf2196f3),
...@@ -994,7 +994,7 @@ void main() { ...@@ -994,7 +994,7 @@ void main() {
expect(find.byType(EditableText), paints expect(find.byType(EditableText), paints
..rrect( ..rrect(
rrect: RRect.fromRectAndRadius( rrect: RRect.fromRectAndRadius(
const Rect.fromLTRB(719.3333333333333, -0.9166666666666679, 721.3333333333333, 17.083333333333332), const Rect.fromLTWH(719.3333333333333, -0.9166666666666679, 2.0, 18.0),
const Radius.circular(2.0), const Radius.circular(2.0),
), ),
color: const Color(0xff999999), color: const Color(0xff999999),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment