@@ -8,12 +8,121 @@ import 'package:flutter/foundation.dart';
8
8
import 'package:flutter/widgets.dart' ;
9
9
import 'package:flutter_test/flutter_test.dart' ;
10
10
11
+ void _checkCaretOffsetsLtrAt (String text, List <int > boundaries) {
12
+ expect (boundaries.first, 0 );
13
+ expect (boundaries.last, text.length);
14
+
15
+ final TextPainter painter = TextPainter ()
16
+ ..textDirection = TextDirection .ltr;
17
+
18
+ // Lay out the string up to each boundary, and record the width.
19
+ final List <double > prefixWidths = < double > [];
20
+ for (final int boundary in boundaries) {
21
+ painter.text = TextSpan (text: text.substring (0 , boundary));
22
+ painter.layout ();
23
+ prefixWidths.add (painter.width);
24
+ }
25
+
26
+ // The painter has the full text laid out. Check the caret offsets.
27
+ double caretOffset (int offset) {
28
+ final TextPosition position = ui.TextPosition (offset: offset);
29
+ return painter.getOffsetForCaret (position, ui.Rect .zero).dx;
30
+ }
31
+ expect (boundaries.map (caretOffset).toList (), prefixWidths);
32
+ double lastOffset = caretOffset (0 );
33
+ for (int i = 1 ; i <= text.length; i++ ) {
34
+ final double offset = caretOffset (i);
35
+ expect (offset, greaterThanOrEqualTo (lastOffset));
36
+ lastOffset = offset;
37
+ }
38
+ painter.dispose ();
39
+ }
40
+
41
+ /// Check the caret offsets are accurate for the given single line of LTR text.
42
+ ///
43
+ /// This lays out the given text as a single line with [TextDirection.ltr]
44
+ /// and checks the following invariants, which should always hold if the text
45
+ /// is made up of LTR characters:
46
+ /// * The caret offsets go monotonically from 0.0 to the width of the text.
47
+ /// * At each character (that is, grapheme cluster) boundary, the caret
48
+ /// offset equals the width that the text up to that point would have
49
+ /// if laid out on its own.
50
+ ///
51
+ /// If you have a [TextSpan] instead of a plain [String] ,
52
+ /// see [caretOffsetsForTextSpan] .
53
+ void checkCaretOffsetsLtr (String text) {
54
+ final List <int > characterBoundaries = < int > [];
55
+ final CharacterRange range = CharacterRange .at (text, 0 );
56
+ while (true ) {
57
+ characterBoundaries.add (range.current.length);
58
+ if (range.stringAfterLength <= 0 ) {
59
+ break ;
60
+ }
61
+ range.expandNext ();
62
+ }
63
+ _checkCaretOffsetsLtrAt (text, characterBoundaries);
64
+ }
65
+
66
+ /// Check the caret offsets are accurate for the given single line of LTR text,
67
+ /// ignoring character boundaries within each given cluster.
68
+ ///
69
+ /// This concatenates [clusters] into a string and then performs the same
70
+ /// checks as [checkCaretOffsetsLtr] , except that instead of checking the
71
+ /// offset-equals-prefix-width invariant at every character boundary,
72
+ /// it does so only at the boundaries between the elements of [clusters] .
73
+ ///
74
+ /// The elements of [clusters] should be composed of whole characters: each
75
+ /// element should be a valid character range in the concatenated string.
76
+ ///
77
+ /// Consider using [checkCaretOffsetsLtr] instead of this function. If that
78
+ /// doesn't pass, you may have an instance of <https://github.com/flutter/flutter/issues/122478>.
79
+ void checkCaretOffsetsLtrFromPieces (List <String > clusters) {
80
+ final StringBuffer buffer = StringBuffer ();
81
+ final List <int > boundaries = < int > [];
82
+ boundaries.add (buffer.length);
83
+ for (final String cluster in clusters) {
84
+ buffer.write (cluster);
85
+ boundaries.add (buffer.length);
86
+ }
87
+ _checkCaretOffsetsLtrAt (buffer.toString (), boundaries);
88
+ }
89
+
90
+ /// Compute the caret offsets for the given single line of text, a [TextSpan] .
91
+ ///
92
+ /// This lays out the given text as a single line with the given [textDirection]
93
+ /// and returns a full list of caret offsets, one at each code unit boundary.
94
+ ///
95
+ /// This also checks that the offset at the very start or very end, if the text
96
+ /// direction is RTL or LTR respectively, equals the line's width.
97
+ ///
98
+ /// If you have a [String] instead of a nontrivial [TextSpan] ,
99
+ /// consider using [checkCaretOffsetsLtr] instead.
100
+ List <double > caretOffsetsForTextSpan (TextDirection textDirection, TextSpan text) {
101
+ final TextPainter painter = TextPainter ()
102
+ ..textDirection = textDirection
103
+ ..text = text
104
+ ..layout ();
105
+ final int length = text.toPlainText ().length;
106
+ final List <double > result = List <double >.generate (length + 1 , (int offset) {
107
+ final TextPosition position = ui.TextPosition (offset: offset);
108
+ return painter.getOffsetForCaret (position, ui.Rect .zero).dx;
109
+ });
110
+ switch (textDirection) {
111
+ case TextDirection .ltr: expect (result[length], painter.width);
112
+ case TextDirection .rtl: expect (result[0 ], painter.width);
113
+ }
114
+ painter.dispose ();
115
+ return result;
116
+ }
117
+
11
118
void main () {
12
119
test ('TextPainter caret test' , () {
13
120
final TextPainter painter = TextPainter ()
14
121
..textDirection = TextDirection .ltr;
15
122
16
123
String text = 'A' ;
124
+ checkCaretOffsetsLtr (text);
125
+
17
126
painter.text = TextSpan (text: text);
18
127
painter.layout ();
19
128
@@ -28,6 +137,7 @@ void main() {
28
137
// Check that getOffsetForCaret handles a character that is encoded as a
29
138
// surrogate pair.
30
139
text = 'A\u {1F600}' ;
140
+ checkCaretOffsetsLtr (text);
31
141
painter.text = TextSpan (text: text);
32
142
painter.layout ();
33
143
caretOffset = painter.getOffsetForCaret (ui.TextPosition (offset: text.length), ui.Rect .zero);
@@ -87,6 +197,8 @@ void main() {
87
197
// Format: '👩<zwj>👩<zwj>👦👩<zwj>👩<zwj>👧<zwj>👧👏<modifier>'
88
198
// One three-person family, one four-person family, one clapping hands (medium skin tone).
89
199
const String text = '👩👩👦👩👩👧👧👏🏽' ;
200
+ checkCaretOffsetsLtr (text);
201
+
90
202
painter.text = const TextSpan (text: text);
91
203
painter.layout (maxWidth: 10000 );
92
204
@@ -147,6 +259,90 @@ void main() {
147
259
painter.dispose ();
148
260
}, skip: isBrowser && ! isCanvasKit); // https://github.com/flutter/flutter/issues/56308
149
261
262
+ test ('TextPainter caret emoji tests: single, long emoji' , () {
263
+ // Regression test for https://github.com/flutter/flutter/issues/50563
264
+ checkCaretOffsetsLtr ('👩🚀' );
265
+ checkCaretOffsetsLtr ('👩❤️💋👩' );
266
+ checkCaretOffsetsLtr ('👨👩👦👦' );
267
+ checkCaretOffsetsLtr ('👨🏾🤝👨🏻' );
268
+ checkCaretOffsetsLtr ('👨👦' );
269
+ checkCaretOffsetsLtr ('👩👦' );
270
+ checkCaretOffsetsLtr ('🏌🏿♀️' );
271
+ checkCaretOffsetsLtr ('🏊♀️' );
272
+ checkCaretOffsetsLtr ('🏄🏻♂️' );
273
+
274
+ // These actually worked even before #50563 was fixed (because
275
+ // their lengths in code units are powers of 2, namely 4 and 8).
276
+ checkCaretOffsetsLtr ('🇺🇳' );
277
+ checkCaretOffsetsLtr ('👩❤️👨' );
278
+ }, skip: isBrowser && ! isCanvasKit); // https://github.com/flutter/flutter/issues/56308
279
+
280
+ test ('TextPainter caret emoji test: letters, then 1 emoji of 5 code units' , () {
281
+ // Regression test for https://github.com/flutter/flutter/issues/50563
282
+ checkCaretOffsetsLtr ('a👩🚀' );
283
+ checkCaretOffsetsLtr ('ab👩🚀' );
284
+ checkCaretOffsetsLtr ('abc👩🚀' );
285
+ checkCaretOffsetsLtr ('abcd👩🚀' );
286
+ }, skip: isBrowser && ! isCanvasKit); // https://github.com/flutter/flutter/issues/56308
287
+
288
+ test ('TextPainter caret zalgo test' , () {
289
+ // Regression test for https://github.com/flutter/flutter/issues/98516
290
+ checkCaretOffsetsLtr ('Z͉̳̺ͥͬ̾a̴͕̲̒̒͌̋ͪl̨͎̰̘͉̟ͤ̀̈̚͜g͕͔̤͖̟̒͝ͅo̵̡̡̼͚̐ͯ̅ͪ̆ͣ̚' );
291
+ }, skip: isBrowser && ! isCanvasKit); // https://github.com/flutter/flutter/issues/56308
292
+
293
+ test ('TextPainter caret Devanagari test' , () {
294
+ // Regression test for https://github.com/flutter/flutter/issues/118403
295
+ checkCaretOffsetsLtrFromPieces (
296
+ < String > ['प्रा' , 'प्त' , ' ' , 'व' , 'र्ण' , 'न' , ' ' , 'प्र' , 'व्रु' , 'ति' ]);
297
+ }, skip: isBrowser && ! isCanvasKit); // https://github.com/flutter/flutter/issues/56308
298
+
299
+ test ('TextPainter caret Devanagari test, full strength' , () {
300
+ // Regression test for https://github.com/flutter/flutter/issues/118403
301
+ checkCaretOffsetsLtr ('प्राप्त वर्णन प्रव्रुति' );
302
+ }, skip: true ); // https://github.com/flutter/flutter/issues/122478
303
+
304
+ test ('TextPainter caret emoji test LTR: letters next to emoji, as separate TextBoxes' , () {
305
+ // Regression test for https://github.com/flutter/flutter/issues/122477
306
+ // The trigger for this bug was to have SkParagraph report separate
307
+ // TextBoxes for the emoji and for the characters next to it.
308
+ // In normal usage on a real device, this can happen by simply typing
309
+ // letters and then an emoji, presumably because they get different fonts.
310
+ // In these tests, our single test font covers both letters and emoji,
311
+ // so we provoke the same effect by adding styles.
312
+ expect (caretOffsetsForTextSpan (
313
+ TextDirection .ltr,
314
+ const TextSpan (children: < TextSpan > [
315
+ TextSpan (text: '👩🚀' , style: TextStyle ()),
316
+ TextSpan (text: ' words' , style: TextStyle (fontWeight: FontWeight .bold)),
317
+ ])),
318
+ < double > [0 , 28 , 28 , 28 , 28 , 28 , 42 , 56 , 70 , 84 , 98 , 112 ]);
319
+ expect (caretOffsetsForTextSpan (
320
+ TextDirection .ltr,
321
+ const TextSpan (children: < TextSpan > [
322
+ TextSpan (text: 'words ' , style: TextStyle (fontWeight: FontWeight .bold)),
323
+ TextSpan (text: '👩🚀' , style: TextStyle ()),
324
+ ])),
325
+ < double > [0 , 14 , 28 , 42 , 56 , 70 , 84 , 84 , 84 , 84 , 84 , 112 ]);
326
+ }, skip: isBrowser && ! isCanvasKit); // https://github.com/flutter/flutter/issues/56308
327
+
328
+ test ('TextPainter caret emoji test RTL: letters next to emoji, as separate TextBoxes' , () {
329
+ // Regression test for https://github.com/flutter/flutter/issues/122477
330
+ expect (caretOffsetsForTextSpan (
331
+ TextDirection .rtl,
332
+ const TextSpan (children: < TextSpan > [
333
+ TextSpan (text: '👩🚀' , style: TextStyle ()),
334
+ TextSpan (text: ' מילים' , style: TextStyle (fontWeight: FontWeight .bold)),
335
+ ])),
336
+ < double > [112 , 84 , 84 , 84 , 84 , 84 , 70 , 56 , 42 , 28 , 14 , 0 ]);
337
+ expect (caretOffsetsForTextSpan (
338
+ TextDirection .rtl,
339
+ const TextSpan (children: < TextSpan > [
340
+ TextSpan (text: 'מילים ' , style: TextStyle (fontWeight: FontWeight .bold)),
341
+ TextSpan (text: '👩🚀' , style: TextStyle ()),
342
+ ])),
343
+ < double > [112 , 98 , 84 , 70 , 56 , 42 , 28 , 28 , 28 , 28 , 28 , 0 ]);
344
+ }, skip: isBrowser && ! isCanvasKit); // https://github.com/flutter/flutter/issues/56308
345
+
150
346
test ('TextPainter caret center space test' , () {
151
347
final TextPainter painter = TextPainter ()
152
348
..textDirection = TextDirection .ltr;
0 commit comments