text_painter_test.dart 41.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:ui' as ui;

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/widgets.dart';
9 10
import 'package:flutter_test/flutter_test.dart';

11
const bool isCanvasKit =
12
    bool.fromEnvironment('FLUTTER_WEB_USE_SKIA');
13

14
void main() {
15
  test('TextPainter caret test', () {
16
    final TextPainter painter = TextPainter()
Ian Hickson's avatar
Ian Hickson committed
17
      ..textDirection = TextDirection.ltr;
18 19

    String text = 'A';
20
    painter.text = TextSpan(text: text);
21 22
    painter.layout();

23 24 25 26
    Offset caretOffset = painter.getOffsetForCaret(
      const ui.TextPosition(offset: 0),
      ui.Rect.zero,
    );
27
    expect(caretOffset.dx, 0);
28
    caretOffset = painter.getOffsetForCaret(ui.TextPosition(offset: text.length), ui.Rect.zero);
29 30
    expect(caretOffset.dx, painter.width);

31 32
    // Check that getOffsetForCaret handles a character that is encoded as a
    // surrogate pair.
33
    text = 'A\u{1F600}';
34
    painter.text = TextSpan(text: text);
35
    painter.layout();
36
    caretOffset = painter.getOffsetForCaret(ui.TextPosition(offset: text.length), ui.Rect.zero);
37
    expect(caretOffset.dx, painter.width);
38
  });
39

40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
  test('TextPainter caret test with WidgetSpan', () {
    // Regression test for https://github.com/flutter/flutter/issues/98458.
    final TextPainter painter = TextPainter()
      ..textDirection = TextDirection.ltr;

    painter.text = const TextSpan(children: <InlineSpan>[
      TextSpan(text: 'before'),
      WidgetSpan(child: Text('widget')),
      TextSpan(text: 'after'),
    ]);
    painter.setPlaceholderDimensions(const <PlaceholderDimensions>[
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
    ]);
    painter.layout();
    final Offset caretOffset = painter.getOffsetForCaret(ui.TextPosition(offset: painter.text!.toPlainText().length), ui.Rect.zero);
    expect(caretOffset.dx, painter.width);
  }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308

58 59 60 61 62
  test('TextPainter null text test', () {
    final TextPainter painter = TextPainter()
      ..textDirection = TextDirection.ltr;

    List<TextSpan> children = <TextSpan>[const TextSpan(text: 'B'), const TextSpan(text: 'C')];
63
    painter.text = TextSpan(children: children);
64 65 66 67 68 69 70 71 72 73
    painter.layout();

    Offset caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 0), ui.Rect.zero);
    expect(caretOffset.dx, 0);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 1), ui.Rect.zero);
    expect(caretOffset.dx, painter.width / 2);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 2), ui.Rect.zero);
    expect(caretOffset.dx, painter.width);

    children = <TextSpan>[];
74
    painter.text = TextSpan(children: children);
75 76 77 78 79 80
    painter.layout();

    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 0), ui.Rect.zero);
    expect(caretOffset.dx, 0);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 1), ui.Rect.zero);
    expect(caretOffset.dx, 0);
81
  });
82

83 84 85 86
  test('TextPainter caret emoji test', () {
    final TextPainter painter = TextPainter()
      ..textDirection = TextDirection.ltr;

87 88 89
    // Format: '👩‍<zwj>👩‍<zwj>👦👩‍<zwj>👩‍<zwj>👧‍<zwj>👧👏<modifier>'
    // One three-person family, one four-person family, one clapping hands (medium skin tone).
    const String text = '👩‍👩‍👦👩‍👩‍👧‍👧👏🏽';
90
    painter.text = const TextSpan(text: text);
91
    painter.layout(maxWidth: 10000);
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137

    expect(text.length, 23);

    Offset caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 0), ui.Rect.zero);
    expect(caretOffset.dx, 0); // 👩‍
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: text.length), ui.Rect.zero);
    expect(caretOffset.dx, painter.width);

    // Two UTF-16 codepoints per emoji, one codepoint per zwj
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 1), ui.Rect.zero);
    expect(caretOffset.dx, 42); // 👩‍
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 2), ui.Rect.zero);
    expect(caretOffset.dx, 42); // <zwj>
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 3), ui.Rect.zero);
    expect(caretOffset.dx, 42); // 👩‍
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 4), ui.Rect.zero);
    expect(caretOffset.dx, 42); // 👩‍
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 5), ui.Rect.zero);
    expect(caretOffset.dx, 42); // <zwj>
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 6), ui.Rect.zero);
    expect(caretOffset.dx, 42); // 👦
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 7), ui.Rect.zero);
    expect(caretOffset.dx, 42); // 👦
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 8), ui.Rect.zero);
    expect(caretOffset.dx, 42); // 👩‍
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 9), ui.Rect.zero);
    expect(caretOffset.dx, 98); // 👩‍
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 10), ui.Rect.zero);
    expect(caretOffset.dx, 98); // <zwj>
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 11), ui.Rect.zero);
    expect(caretOffset.dx, 98); // 👩‍
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 12), ui.Rect.zero);
    expect(caretOffset.dx, 98); // 👩‍
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 13), ui.Rect.zero);
    expect(caretOffset.dx, 98); // <zwj>
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 14), ui.Rect.zero);
    expect(caretOffset.dx, 98); // 👧‍
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 15), ui.Rect.zero);
    expect(caretOffset.dx, 98); // 👧‍
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 16), ui.Rect.zero);
    expect(caretOffset.dx, 98); // <zwj>
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 17), ui.Rect.zero);
    expect(caretOffset.dx, 98); // 👧
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 18), ui.Rect.zero);
    expect(caretOffset.dx, 98); // 👧
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 19), ui.Rect.zero);
138
    expect(caretOffset.dx, 98); // 👏
139
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 20), ui.Rect.zero);
140
    expect(caretOffset.dx, 98); // 👏
141
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 21), ui.Rect.zero);
142
    expect(caretOffset.dx, 98); // <medium skin tone modifier>
143
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 22), ui.Rect.zero);
144 145 146
    expect(caretOffset.dx, 98); // <medium skin tone modifier>
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 23), ui.Rect.zero);
    expect(caretOffset.dx, 126); // end of string
147
  }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308
148 149 150 151 152 153 154 155 156 157 158 159 160

  test('TextPainter caret center space test', () {
    final TextPainter painter = TextPainter()
      ..textDirection = TextDirection.ltr;

    const String text = 'test text with space at end   ';
    painter.text = const TextSpan(text: text);
    painter.textAlign = TextAlign.center;
    painter.layout();

    Offset caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 0), ui.Rect.zero);
    expect(caretOffset.dx, 21);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: text.length), ui.Rect.zero);
161 162 163
    // The end of the line is 441, but the width is only 420, so the cursor is
    // stopped there without overflowing.
    expect(caretOffset.dx, painter.width);
164 165 166 167 168

    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 1), ui.Rect.zero);
    expect(caretOffset.dx, 35);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 2), ui.Rect.zero);
    expect(caretOffset.dx, 49);
169
  }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308
170

171
  test('TextPainter error test', () {
172
    final TextPainter painter = TextPainter(textDirection: TextDirection.ltr);
173 174 175 176 177 178 179 180 181 182
    Object? e;
    try {
      painter.paint(MockCanvas(), Offset.zero);
    } catch (exception) {
      e = exception;
    }
    expect(
      e.toString(),
      contains('TextPainter.paint called when text geometry was not yet calculated'),
    );
183
  });
184

Ian Hickson's avatar
Ian Hickson committed
185
  test('TextPainter requires textDirection', () {
186
    final TextPainter painter1 = TextPainter(text: const TextSpan(text: ''));
Ian Hickson's avatar
Ian Hickson committed
187
    expect(() { painter1.layout(); }, throwsAssertionError);
188
    final TextPainter painter2 = TextPainter(text: const TextSpan(text: ''), textDirection: TextDirection.rtl);
Ian Hickson's avatar
Ian Hickson committed
189 190 191
    expect(() { painter2.layout(); }, isNot(throwsException));
  });

192
  test('TextPainter size test', () {
193
    final TextPainter painter = TextPainter(
194
      text: const TextSpan(
195
        text: 'X',
196
        style: TextStyle(
197 198 199 200 201
          inherit: false,
          fontFamily: 'Ahem',
          fontSize: 123.0,
        ),
      ),
Ian Hickson's avatar
Ian Hickson committed
202
      textDirection: TextDirection.ltr,
203 204 205
    );
    painter.layout();
    expect(painter.size, const Size(123.0, 123.0));
206
  });
207

208
  test('TextPainter textScaleFactor test', () {
209
    final TextPainter painter = TextPainter(
210 211
      text: const TextSpan(
        text: 'X',
212
        style: TextStyle(
213 214 215 216 217 218 219 220 221 222
          inherit: false,
          fontFamily: 'Ahem',
          fontSize: 10.0,
        ),
      ),
      textDirection: TextDirection.ltr,
      textScaleFactor: 2.0,
    );
    painter.layout();
    expect(painter.size, const Size(20.0, 20.0));
223
  });
224

225 226 227 228 229 230 231 232 233 234 235 236
  test('TextPainter textScaleFactor null style test', () {
    final TextPainter painter = TextPainter(
      text: const TextSpan(
        text: 'X',
      ),
      textDirection: TextDirection.ltr,
      textScaleFactor: 2.0,
    );
    painter.layout();
    expect(painter.size, const Size(28.0, 28.0));
  });

237
  test('TextPainter default text height is 14 pixels', () {
238
    final TextPainter painter = TextPainter(
Ian Hickson's avatar
Ian Hickson committed
239 240 241
      text: const TextSpan(text: 'x'),
      textDirection: TextDirection.ltr,
    );
242 243 244
    painter.layout();
    expect(painter.preferredLineHeight, 14.0);
    expect(painter.size, const Size(14.0, 14.0));
245
  });
246 247

  test('TextPainter sets paragraph size from root', () {
248
    final TextPainter painter = TextPainter(
249
      text: const TextSpan(text: 'x', style: TextStyle(fontSize: 100.0)),
Ian Hickson's avatar
Ian Hickson committed
250 251
      textDirection: TextDirection.ltr,
    );
252 253 254
    painter.layout();
    expect(painter.preferredLineHeight, 100.0);
    expect(painter.size, const Size(100.0, 100.0));
255
  });
256 257

  test('TextPainter intrinsic dimensions', () {
258
    const TextStyle style = TextStyle(
259 260 261 262 263 264
      inherit: false,
      fontFamily: 'Ahem',
      fontSize: 10.0,
    );
    TextPainter painter;

265
    painter = TextPainter(
266 267 268 269 270 271 272 273 274 275 276
      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);

277
    painter = TextPainter(
278 279 280 281 282 283 284 285 286 287 288 289
      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);

290
    painter = TextPainter(
291 292 293 294 295 296 297 298 299 300 301 302
      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);

303
    painter = TextPainter(
304 305 306 307 308 309 310 311 312 313 314 315
      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);

316
    painter = TextPainter(
317 318 319 320 321 322 323 324 325 326 327 328
      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);

329
    painter = TextPainter(
330 331 332 333 334 335 336 337 338 339 340 341
      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);
  }, skip: true); // https://github.com/flutter/flutter/issues/13512
342 343

  test('TextPainter handles newlines properly', () {
344
    final TextPainter painter = TextPainter()
345 346
      ..textDirection = TextDirection.ltr;

347
    const double SIZE_OF_A = 14.0; // square size of "a" character
348
    String text = 'aaa';
349
    painter.text = TextSpan(text: text);
350 351
    painter.layout();

352 353 354 355 356 357 358 359
    // getOffsetForCaret in a plain one-line string is the same for either affinity.
    int offset = 0;
    painter.text = TextSpan(text: text);
    painter.layout();
    Offset caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
360 361
    expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
362 363 364 365
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
366 367
    expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
368 369 370 371 372
    offset = 1;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
373 374
    expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
375 376 377 378
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
379 380
    expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
381 382 383 384 385
    offset = 2;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
386 387
    expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
388 389 390 391
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
392 393
    expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
394 395 396 397 398
    offset = 3;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
399 400
    expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
401 402 403 404
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
405 406
    expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * offset, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
407

408 409 410
    // For explicit newlines, getOffsetForCaret places the caret at the location
    // indicated by offset regardless of affinity.
    text = '\n\n';
411
    painter.text = TextSpan(text: text);
412
    painter.layout();
413 414 415 416 417
    offset = 0;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
418 419
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
420 421 422 423
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
424 425
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
426 427 428 429 430
    offset = 1;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
431 432
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001));
433 434 435 436
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
437 438
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001));
439 440 441 442 443
    offset = 2;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
444 445
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 2, epsilon: 0.0001));
446 447 448 449
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
450 451
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 2, epsilon: 0.0001));
452

453 454 455
    // getOffsetForCaret in an unwrapped string with explicit newlines is the
    // same for either affinity.
    text = '\naaa';
456
    painter.text = TextSpan(text: text);
457
    painter.layout();
458 459 460 461 462
    offset = 0;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
463 464
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
465 466 467 468
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
469 470
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
471 472 473 474 475
    offset = 1;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
476 477
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001));
478 479 480 481
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
482 483
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001));
484

485 486 487
    // When text wraps on its own, getOffsetForCaret disambiguates between the
    // end of one line and start of next using affinity.
    text = 'aaaaaaaa'; // Just enough to wrap one character down to second line
488
    painter.text = TextSpan(text: text);
489 490 491 492 493 494
    painter.layout(maxWidth: 100); // SIZE_OF_A * text.length > 100, so it wraps
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: text.length - 1),
      ui.Rect.zero,
    );
    // When affinity is downstream, cursor is at beginning of second line
495 496
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001));
497 498 499 500 501
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: text.length - 1, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
    // When affinity is upstream, cursor is at end of first line
502 503
    expect(caretOffset.dx, moreOrLessEquals(98.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
504

505 506 507
    // When given a string with a newline at the end, getOffsetForCaret puts
    // the cursor at the start of the next line regardless of affinity
    text = 'aaa\n';
508
    painter.text = TextSpan(text: text);
509
    painter.layout();
510 511 512 513
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: text.length),
      ui.Rect.zero,
    );
514 515
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001));
516 517 518 519 520
    offset = text.length;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
521 522
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001));
523

524 525 526 527 528 529 530 531 532 533 534 535
    // 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
    // that was typed first, at x=0.
    painter.textAlign = TextAlign.right;
    text = 'aaa';
    painter.text = TextSpan(text: text);
    painter.layout();
    offset = 0;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
536 537
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
538
    painter.textAlign = TextAlign.left;
539

540 541 542 543 544 545 546 547 548 549 550
    // When given an offset after a newline in the middle of a string,
    // getOffsetForCaret returns the start of the next line regardless of
    // affinity.
    text = 'aaa\naaa';
    painter.text = TextSpan(text: text);
    painter.layout();
    offset = 4;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
551 552
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001));
553 554 555 556
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
557 558
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001));
559

560 561 562 563 564 565 566 567
    // When given a string with multiple trailing newlines, places the caret
    // in the position given by offset regardless of affinity.
    text = 'aaa\n\n\n';
    offset = 3;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
568 569
    expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * 3, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
570 571 572 573
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
574 575
    expect(caretOffset.dx, moreOrLessEquals(SIZE_OF_A * 3, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
576

577
    offset = 4;
578
    painter.text = TextSpan(text: text);
579
    painter.layout();
580 581 582 583
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
584 585
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.001));
586 587 588 589
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
590 591
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001));
592

593 594 595 596 597
    offset = 5;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
598 599
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 2, epsilon: 0.001));
600 601 602 603
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
604 605
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 2, epsilon: 0.0001));
606

607 608 609 610 611
    offset = 6;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
612 613
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 3, epsilon: 0.0001));
614

615 616 617 618
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
619 620
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 3, epsilon: 0.0001));
621

622 623 624 625 626 627 628 629 630 631
    // When given a string with multiple leading newlines, places the caret in
    // the position given by offset regardless of affinity.
    text = '\n\n\naaa';
    offset = 3;
    painter.text = TextSpan(text: text);
    painter.layout();
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
632 633
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 3, epsilon: 0.0001));
634 635 636 637
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
638 639
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 3, epsilon: 0.0001));
640

641 642 643 644 645
    offset = 2;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
646 647
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 2, epsilon: 0.0001));
648 649 650 651
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
652 653
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A * 2, epsilon: 0.0001));
654

655 656 657 658 659
    offset = 1;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
660 661
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy,moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001));
662 663 664 665
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
666 667
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(SIZE_OF_A, epsilon: 0.0001));
668

669 670 671 672 673
    offset = 0;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
674 675
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
676 677 678 679
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
680 681
    expect(caretOffset.dx, moreOrLessEquals(0.0, epsilon: 0.0001));
    expect(caretOffset.dy, moreOrLessEquals(0.0, epsilon: 0.0001));
682
  });
683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707

  test('TextPainter widget span', () {
    final TextPainter painter = TextPainter()
      ..textDirection = TextDirection.ltr;

    const String text = 'test';
    painter.text = const TextSpan(
      text: text,
      children: <InlineSpan>[
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
        TextSpan(text: text),
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
        TextSpan(text: text),
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
        WidgetSpan(child: SizedBox(width: 50, height: 30)),
708
      ],
709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762
    );

    // We provide dimensions for the widgets
    painter.setPlaceholderDimensions(const <PlaceholderDimensions>[
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
      PlaceholderDimensions(size: Size(51, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
      PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
    ]);

    painter.layout(maxWidth: 500);

    // Now, each of the WidgetSpans will have their own placeholder 'hole'.
    Offset caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 1), ui.Rect.zero);
    expect(caretOffset.dx, 14);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 4), ui.Rect.zero);
    expect(caretOffset.dx, 56);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 5), ui.Rect.zero);
    expect(caretOffset.dx, 106);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 6), ui.Rect.zero);
    expect(caretOffset.dx, 120);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 10), ui.Rect.zero);
    expect(caretOffset.dx, 212);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 11), ui.Rect.zero);
    expect(caretOffset.dx, 262);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 12), ui.Rect.zero);
    expect(caretOffset.dx, 276);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 13), ui.Rect.zero);
    expect(caretOffset.dx, 290);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 14), ui.Rect.zero);
    expect(caretOffset.dx, 304);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 15), ui.Rect.zero);
    expect(caretOffset.dx, 318);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 16), ui.Rect.zero);
    expect(caretOffset.dx, 368);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 17), ui.Rect.zero);
    expect(caretOffset.dx, 418);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 18), ui.Rect.zero);
    expect(caretOffset.dx, 0);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 19), ui.Rect.zero);
    expect(caretOffset.dx, 50);
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 23), ui.Rect.zero);
    expect(caretOffset.dx, 250);

763 764 765 766 767 768
    expect(painter.inlinePlaceholderBoxes!.length, 14);
    expect(painter.inlinePlaceholderBoxes![0], const TextBox.fromLTRBD(56, 0, 106, 30, TextDirection.ltr));
    expect(painter.inlinePlaceholderBoxes![2], const TextBox.fromLTRBD(212, 0, 262, 30, TextDirection.ltr));
    expect(painter.inlinePlaceholderBoxes![3], const TextBox.fromLTRBD(318, 0, 368, 30, TextDirection.ltr));
    expect(painter.inlinePlaceholderBoxes![4], const TextBox.fromLTRBD(368, 0, 418, 30, TextDirection.ltr));
    expect(painter.inlinePlaceholderBoxes![5], const TextBox.fromLTRBD(418, 0, 468, 30, TextDirection.ltr));
769
    // line should break here
770 771 772 773 774 775
    expect(painter.inlinePlaceholderBoxes![6], const TextBox.fromLTRBD(0, 30, 50, 60, TextDirection.ltr));
    expect(painter.inlinePlaceholderBoxes![7], const TextBox.fromLTRBD(50, 30, 100, 60, TextDirection.ltr));
    expect(painter.inlinePlaceholderBoxes![10], const TextBox.fromLTRBD(200, 30, 250, 60, TextDirection.ltr));
    expect(painter.inlinePlaceholderBoxes![11], const TextBox.fromLTRBD(250, 30, 300, 60, TextDirection.ltr));
    expect(painter.inlinePlaceholderBoxes![12], const TextBox.fromLTRBD(300, 30, 351, 60, TextDirection.ltr));
    expect(painter.inlinePlaceholderBoxes![13], const TextBox.fromLTRBD(351, 30, 401, 60, TextDirection.ltr));
776
  }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/87540
777

778 779
  // Null values are valid. See https://github.com/flutter/flutter/pull/48346#issuecomment-584839221
  test('TextPainter set TextHeightBehavior null test', () {
780
    final TextPainter painter = TextPainter()
781 782 783 784 785 786
      ..textDirection = TextDirection.ltr;

    painter.textHeightBehavior = const TextHeightBehavior();
    painter.textHeightBehavior = null;
  });

787 788 789 790 791 792 793 794 795 796 797
  test('TextPainter line metrics', () {
    final TextPainter painter = TextPainter()
      ..textDirection = TextDirection.ltr;

    const String text = 'test1\nhello line two really long for soft break\nfinal line 4';
    painter.text = const TextSpan(
      text: text,
    );

    painter.layout(maxWidth: 300);

798 799 800
    expect(painter.text, const TextSpan(text: text));
    expect(painter.preferredLineHeight, 14);

801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848
    final List<ui.LineMetrics> lines = painter.computeLineMetrics();

    expect(lines.length, 4);

    expect(lines[0].hardBreak, true);
    expect(lines[1].hardBreak, false);
    expect(lines[2].hardBreak, true);
    expect(lines[3].hardBreak, true);

    expect(lines[0].ascent, 11.199999809265137);
    expect(lines[1].ascent, 11.199999809265137);
    expect(lines[2].ascent, 11.199999809265137);
    expect(lines[3].ascent, 11.199999809265137);

    expect(lines[0].descent, 2.799999952316284);
    expect(lines[1].descent, 2.799999952316284);
    expect(lines[2].descent, 2.799999952316284);
    expect(lines[3].descent, 2.799999952316284);

    expect(lines[0].unscaledAscent, 11.199999809265137);
    expect(lines[1].unscaledAscent, 11.199999809265137);
    expect(lines[2].unscaledAscent, 11.199999809265137);
    expect(lines[3].unscaledAscent, 11.199999809265137);

    expect(lines[0].baseline, 11.200000047683716);
    expect(lines[1].baseline, 25.200000047683716);
    expect(lines[2].baseline, 39.200000047683716);
    expect(lines[3].baseline, 53.200000047683716);

    expect(lines[0].height, 14);
    expect(lines[1].height, 14);
    expect(lines[2].height, 14);
    expect(lines[3].height, 14);

    expect(lines[0].width, 70);
    expect(lines[1].width, 294);
    expect(lines[2].width, 266);
    expect(lines[3].width, 168);

    expect(lines[0].left, 0);
    expect(lines[1].left, 0);
    expect(lines[2].left, 0);
    expect(lines[3].left, 0);

    expect(lines[0].lineNumber, 0);
    expect(lines[1].lineNumber, 1);
    expect(lines[2].lineNumber, 2);
    expect(lines[3].lineNumber, 3);
849
  }, skip: true); // https://github.com/flutter/flutter/issues/62819
850 851 852 853 854 855 856 857 858 859 860 861 862

  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,
863
    )!;
864
    expect(caretHeight, 50.0);
865
  }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/56308
866 867

  group('TextPainter line-height', () {
868
    test('half-leading', () {
869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890
      const TextStyle style = TextStyle(
        height: 20,
        fontSize: 1,
        leadingDistribution: TextLeadingDistribution.even,
      );

      final TextPainter painter = TextPainter()
        ..textDirection = TextDirection.ltr
        ..text = const TextSpan(text: 'A', style: style)
        ..layout();

      final Rect glyphBox = painter.getBoxesForSelection(
        const TextSelection(baseOffset: 0, extentOffset: 1),
      ).first.toRect();

      final RelativeRect insets = RelativeRect.fromSize(glyphBox, painter.size);
      // The glyph box is centered.
      expect(insets.top, insets.bottom);
      // The glyph box is exactly 1 logical pixel high.
      expect(insets.top, (20 - 1) / 2);
    });

891
    test('half-leading with small height', () {
892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914
      const TextStyle style = TextStyle(
        height: 0.1,
        fontSize: 10,
        leadingDistribution: TextLeadingDistribution.even,
      );

      final TextPainter painter = TextPainter()
        ..textDirection = TextDirection.ltr
        ..text = const TextSpan(text: 'A', style: style)
        ..layout();

      final Rect glyphBox = painter.getBoxesForSelection(
        const TextSelection(baseOffset: 0, extentOffset: 1),
      ).first.toRect();

      final RelativeRect insets = RelativeRect.fromSize(glyphBox, painter.size);
      // The glyph box is still centered.
      expect(insets.top, insets.bottom);
      // The glyph box is exactly 10 logical pixel high (the height multiplier
      // does not scale the glyph). Negative leading.
      expect(insets.top, (1 - 10) / 2);
    });

915
    test('half-leading with leading trim', () {
916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939
      const TextStyle style = TextStyle(
        height: 0.1,
        fontSize: 10,
        leadingDistribution: TextLeadingDistribution.even,
      );

      final TextPainter painter = TextPainter()
        ..textDirection = TextDirection.ltr
        ..text = const TextSpan(text: 'A', style: style)
        ..textHeightBehavior = const TextHeightBehavior(
            applyHeightToFirstAscent: false,
            applyHeightToLastDescent: false,
          )
        ..layout();

      final Rect glyphBox = painter.getBoxesForSelection(
        const TextSelection(baseOffset: 0, extentOffset: 1),
      ).first.toRect();

      expect(painter.size, glyphBox.size);
      // The glyph box is still centered.
      expect(glyphBox.topLeft, Offset.zero);
    });

940
    test('TextLeadingDistribution falls back to paragraph style', () {
941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959
      const TextStyle style = TextStyle(height: 20, fontSize: 1);
      final TextPainter painter = TextPainter()
        ..textDirection = TextDirection.ltr
        ..text = const TextSpan(text: 'A', style: style)
        ..textHeightBehavior = const TextHeightBehavior(
            leadingDistribution: TextLeadingDistribution.even,
          )
        ..layout();

      final Rect glyphBox = painter.getBoxesForSelection(
        const TextSelection(baseOffset: 0, extentOffset: 1),
      ).first.toRect();

      // Still uses half-leading.
      final RelativeRect insets = RelativeRect.fromSize(glyphBox, painter.size);
      expect(insets.top, insets.bottom);
      expect(insets.top, (20 - 1) / 2);
    });

960
    test('TextLeadingDistribution does nothing if height multiplier is null', () {
961 962 963 964 965 966 967 968 969 970 971 972 973
      const TextStyle style = TextStyle(fontSize: 1);
      final TextPainter painter = TextPainter()
        ..textDirection = TextDirection.ltr
        ..text = const TextSpan(text: 'A', style: style)
        ..textHeightBehavior = const TextHeightBehavior(
            leadingDistribution: TextLeadingDistribution.even,
          )
        ..layout();

      final Rect glyphBox = painter.getBoxesForSelection(
        const TextSelection(baseOffset: 0, extentOffset: 1),
      ).first.toRect();

974
      painter.textHeightBehavior = const TextHeightBehavior();
975 976 977 978 979 980 981
      painter.layout();

      final Rect newGlyphBox = painter.getBoxesForSelection(
        const TextSelection(baseOffset: 0, extentOffset: 1),
      ).first.toRect();
      expect(glyphBox, newGlyphBox);
    });
982
  }, skip: isBrowser && !isCanvasKit); // https://github.com/flutter/flutter/issues/87543
983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999

  test('TextPainter handles invalid UTF-16', () {
    Object? exception;
    FlutterError.onError = (FlutterErrorDetails details) {
      exception = details.exception;
    };

    final TextPainter painter = TextPainter()
      ..textDirection = TextDirection.ltr;

    const String text = 'Hello\uD83DWorld';
    const double fontSize = 20.0;
    painter.text = const TextSpan(text: text, style: TextStyle(fontSize: fontSize));
    painter.layout();
    // The layout should include one replacement character.
    expect(painter.width, equals(fontSize));
    expect(exception, isNotNull);
1000
  }, skip: kIsWeb); // https://github.com/flutter/flutter/issues/87544
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015

  test('Diacritic', () {
    final TextPainter painter = TextPainter()
      ..textDirection = TextDirection.ltr;

    // Two letters followed by a diacritic
    const String text = 'ฟห้';
    painter.text = const TextSpan(text: text);
    painter.layout();

    final ui.Offset caretOffset = painter.getOffsetForCaret(
        const ui.TextPosition(
            offset: text.length, affinity: TextAffinity.upstream),
        ui.Rect.zero);
    expect(caretOffset.dx, painter.width);
1016
  }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/87545
1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036

  test('TextPainter line metrics update after layout', () {
    final TextPainter painter = TextPainter()
      ..textDirection = TextDirection.ltr;

    const String text = 'word1 word2 word3';
    painter.text = const TextSpan(
      text: text,
    );

    painter.layout(maxWidth: 80);

    List<ui.LineMetrics> lines = painter.computeLineMetrics();
    expect(lines.length, 3);

    painter.layout(maxWidth: 1000);

    lines = painter.computeLineMetrics();
    expect(lines.length, 1);
  }, skip: kIsWeb && !isCanvasKit); // https://github.com/flutter/flutter/issues/62819
1037
}
1038 1039 1040 1041

class MockCanvas extends Fake implements Canvas {

}