mirror of
https://github.com/flutter/flutter
synced 2024-08-24 18:36:03 +00:00
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.
This commit is contained in:
parent
c84565a6fa
commit
a0a854a78b
|
@ -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() {
|
||||||
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() {
|
||||||
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() {
|
||||||
}, 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 @@
|
||||||
// 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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
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.
|
|
||||||
|
/// The writing direction of the glyph the _LineCaretMetrics is associated with.
|
||||||
|
/// The value determines whether the cursor is painted to the left or to the
|
||||||
|
/// right of [offset].
|
||||||
final TextDirection writingDirection;
|
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
|
_LineCaretMetrics shift(Offset offset) {
|
||||||
/// empty, or the caret is between two a newline characters).
|
return offset == Offset.zero
|
||||||
final class _EmptyLineCaretMetrics implements _CaretMetrics {
|
? this
|
||||||
const _EmptyLineCaretMetrics({ required this.lineVerticalOffset });
|
: _LineCaretMetrics(offset: offset + this.offset, writingDirection: writingDirection);
|
||||||
|
}
|
||||||
/// The y offset of the unoccupied line.
|
|
||||||
final double lineVerticalOffset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const String _flutterPaintingLibrary = 'package:flutter/painting.dart';
|
const String _flutterPaintingLibrary = 'package:flutter/painting.dart';
|
||||||
|
@ -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 {
|
||||||
);
|
);
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
..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 {
|
||||||
/// 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 {
|
||||||
// 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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
///
|
///
|
||||||
/// 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);
|
if (caretMetrics == null) {
|
||||||
} else {
|
final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection!);
|
||||||
caretMetrics = _computeCaretMetrics(position);
|
// 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 * layoutCache.contentWidth;
|
||||||
|
return Offset(dx, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Offset rawOffset;
|
final Offset rawOffset = switch (caretMetrics) {
|
||||||
switch (caretMetrics) {
|
_LineCaretMetrics(writingDirection: TextDirection.ltr, :final Offset offset) => offset,
|
||||||
case _EmptyLineCaretMetrics(:final double lineVerticalOffset):
|
_LineCaretMetrics(writingDirection: TextDirection.rtl, :final Offset offset) => Offset(offset.dx - caretPrototype.width, offset.dy),
|
||||||
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 * layoutCache.contentWidth;
|
|
||||||
return Offset(dx, lineVerticalOffset);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
// 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 {
|
||||||
///
|
///
|
||||||
/// 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)) {
|
|
||||||
_LineCaretMetrics(:final double fullHeight) => fullHeight,
|
|
||||||
_EmptyLineCaretMetrics() => null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
bool _isNewlineAtOffset(int offset) => 0 <= offset && offset < plainText.length
|
||||||
|
&& WordBoundary._isNewline(plainText.codeUnitAt(offset));
|
||||||
// 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
|
||||||
// Cache the input parameters to prevent repeat work later.
|
// that is not laid out. Use the EOT caret.
|
||||||
cachedLayout._previousCaretPosition = position;
|
// TODO(LongCatIsLooong): assert when an invalid position is given.
|
||||||
return _caretMetrics = metrics ?? const _EmptyLineCaretMetrics(lineVerticalOffset: 0);
|
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,
|
||||||
|
};
|
||||||
|
metrics = _LineCaretMetrics(
|
||||||
|
offset: Offset(dx, graphemeBounds.top),
|
||||||
|
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() {
|
||||||
// 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() {
|
||||||
// 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() {
|
||||||
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() {
|
||||||
key: textFieldKey,
|
key: textFieldKey,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
textDirection: TextDirection.rtl,
|
textDirection: TextDirection.rtl,
|
||||||
|
style: style,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -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() {
|
||||||
).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() {
|
||||||
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(
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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() {
|
||||||
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),
|
||||||
|
|
Loading…
Reference in a new issue