text_painter_test.dart 33.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// 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;

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

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

    String text = 'A';
17
    painter.text = TextSpan(text: text);
18 19
    painter.layout();

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

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

36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
  test('TextPainter null text test', () {
    final TextPainter painter = TextPainter()
      ..textDirection = TextDirection.ltr;

    List<TextSpan> children = <TextSpan>[const TextSpan(text: 'B'), const TextSpan(text: 'C')];
    painter.text = TextSpan(text: null, children: children);
    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>[];
    painter.text = TextSpan(text: null, children: children);
    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);
59
  });
60

61 62 63 64 65 66 67 68
  test('TextPainter caret emoji test', () {
    final TextPainter painter = TextPainter()
      ..textDirection = TextDirection.ltr;

    // Format: '👩‍<zwj>👩‍<zwj>👦👩‍<zwj>👩‍<zwj>👧‍<zwj>👧🇺🇸'
    // One three-person family, one four person family, one US flag.
    const String text = '👩‍👩‍👦👩‍👩‍👧‍👧🇺🇸';
    painter.text = const TextSpan(text: text);
69
    painter.layout(maxWidth: 10000);
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 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

    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);
    expect(caretOffset.dx, 98); // 🇺
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 20), ui.Rect.zero);
    expect(caretOffset.dx, 112); // 🇺
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 21), ui.Rect.zero);
    expect(caretOffset.dx, 112); // 🇸
    caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 22), ui.Rect.zero);
    expect(caretOffset.dx, 112); // 🇸
123
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56308
124 125 126 127 128 129 130 131 132 133 134 135 136

  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);
137 138 139
    // 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);
140 141 142 143 144

    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);
145
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56308
146

147
  test('TextPainter error test', () {
148
    final TextPainter painter = TextPainter(textDirection: TextDirection.ltr);
149
    expect(() { painter.paint(null, Offset.zero); }, throwsFlutterError);
150
  });
151

Ian Hickson's avatar
Ian Hickson committed
152
  test('TextPainter requires textDirection', () {
153
    final TextPainter painter1 = TextPainter(text: const TextSpan(text: ''));
Ian Hickson's avatar
Ian Hickson committed
154
    expect(() { painter1.layout(); }, throwsAssertionError);
155
    final TextPainter painter2 = TextPainter(text: const TextSpan(text: ''), textDirection: TextDirection.rtl);
Ian Hickson's avatar
Ian Hickson committed
156 157 158
    expect(() { painter2.layout(); }, isNot(throwsException));
  });

159
  test('TextPainter size test', () {
160
    final TextPainter painter = TextPainter(
161
      text: const TextSpan(
162
        text: 'X',
163
        style: TextStyle(
164 165 166 167 168
          inherit: false,
          fontFamily: 'Ahem',
          fontSize: 123.0,
        ),
      ),
Ian Hickson's avatar
Ian Hickson committed
169
      textDirection: TextDirection.ltr,
170 171 172
    );
    painter.layout();
    expect(painter.size, const Size(123.0, 123.0));
173
  });
174

175
  test('TextPainter textScaleFactor test', () {
176
    final TextPainter painter = TextPainter(
177 178
      text: const TextSpan(
        text: 'X',
179
        style: TextStyle(
180 181 182 183 184 185 186 187 188 189
          inherit: false,
          fontFamily: 'Ahem',
          fontSize: 10.0,
        ),
      ),
      textDirection: TextDirection.ltr,
      textScaleFactor: 2.0,
    );
    painter.layout();
    expect(painter.size, const Size(20.0, 20.0));
190
  });
191

192
  test('TextPainter default text height is 14 pixels', () {
193
    final TextPainter painter = TextPainter(
Ian Hickson's avatar
Ian Hickson committed
194 195 196
      text: const TextSpan(text: 'x'),
      textDirection: TextDirection.ltr,
    );
197 198 199
    painter.layout();
    expect(painter.preferredLineHeight, 14.0);
    expect(painter.size, const Size(14.0, 14.0));
200
  });
201 202

  test('TextPainter sets paragraph size from root', () {
203
    final TextPainter painter = TextPainter(
204
      text: const TextSpan(text: 'x', style: TextStyle(fontSize: 100.0)),
Ian Hickson's avatar
Ian Hickson committed
205 206
      textDirection: TextDirection.ltr,
    );
207 208 209
    painter.layout();
    expect(painter.preferredLineHeight, 100.0);
    expect(painter.size, const Size(100.0, 100.0));
210
  });
211 212

  test('TextPainter intrinsic dimensions', () {
213
    const TextStyle style = TextStyle(
214 215 216 217 218 219
      inherit: false,
      fontFamily: 'Ahem',
      fontSize: 10.0,
    );
    TextPainter painter;

220
    painter = TextPainter(
221 222 223 224 225 226 227 228 229 230 231
      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);

232
    painter = TextPainter(
233 234 235 236 237 238 239 240 241 242 243 244
      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);

245
    painter = TextPainter(
246 247 248 249 250 251 252 253 254 255 256 257
      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);

258
    painter = TextPainter(
259 260 261 262 263 264 265 266 267 268 269 270
      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);

271
    painter = TextPainter(
272 273 274 275 276 277 278 279 280 281 282 283
      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);

284
    painter = TextPainter(
285 286 287 288 289 290 291 292 293 294 295 296
      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
297 298

  test('TextPainter handles newlines properly', () {
299
    final TextPainter painter = TextPainter()
300 301
      ..textDirection = TextDirection.ltr;

302
    const double SIZE_OF_A = 14.0; // square size of "a" character
303
    String text = 'aaa';
304
    painter.text = TextSpan(text: text);
305 306
    painter.layout();

307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
    // 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,
    );
    expect(caretOffset.dx, closeTo(SIZE_OF_A * offset, 0.0001));
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(SIZE_OF_A * offset, 0.0001));
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
    offset = 1;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(SIZE_OF_A * offset, 0.0001));
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(SIZE_OF_A * offset, 0.0001));
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
    offset = 2;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(SIZE_OF_A * offset, 0.0001));
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(SIZE_OF_A * offset, 0.0001));
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
    offset = 3;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(SIZE_OF_A * offset, 0.0001));
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(SIZE_OF_A * offset, 0.0001));
361 362
    expect(caretOffset.dy, closeTo(0.0, 0.0001));

363 364 365
    // For explicit newlines, getOffsetForCaret places the caret at the location
    // indicated by offset regardless of affinity.
    text = '\n\n';
366
    painter.text = TextSpan(text: text);
367
    painter.layout();
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
    offset = 0;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
    offset = 1;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(SIZE_OF_A, 0.0001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(SIZE_OF_A, 0.0001));
    offset = 2;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(SIZE_OF_A * 2, 0.0001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
405
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
406
    expect(caretOffset.dy, closeTo(SIZE_OF_A * 2, 0.0001));
407

408 409 410
    // getOffsetForCaret in an unwrapped string with explicit newlines is the
    // same for either affinity.
    text = '\naaa';
411
    painter.text = TextSpan(text: text);
412
    painter.layout();
413 414 415 416 417 418
    offset = 0;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
419
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
    offset = 1;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(SIZE_OF_A, 0.0001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: ui.TextAffinity.upstream),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(SIZE_OF_A, 0.0001));
439

440 441 442
    // 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
443
    painter.text = TextSpan(text: text);
444 445 446 447 448 449
    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
450
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
451 452 453 454 455 456 457
    expect(caretOffset.dy, closeTo(SIZE_OF_A, 0.0001));
    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
    expect(caretOffset.dx, closeTo(98.0, 0.0001));
458 459
    expect(caretOffset.dy, closeTo(0.0, 0.0001));

460 461 462
    // 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';
463
    painter.text = TextSpan(text: text);
464
    painter.layout();
465 466 467 468
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: text.length),
      ui.Rect.zero,
    );
469
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
470 471 472 473 474 475
    expect(caretOffset.dy, closeTo(SIZE_OF_A, 0.0001));
    offset = text.length;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
476
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
477
    expect(caretOffset.dy, closeTo(SIZE_OF_A, 0.0001));
478

479 480 481 482 483 484 485 486 487 488 489 490
    // 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,
    );
491
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
492 493
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
    painter.textAlign = TextAlign.left;
494

495 496 497 498 499 500 501 502 503 504 505
    // 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,
    );
506
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
507 508 509 510 511
    expect(caretOffset.dy, closeTo(SIZE_OF_A, 0.0001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
512
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
513
    expect(caretOffset.dy, closeTo(SIZE_OF_A, 0.0001));
514

515 516 517 518 519 520 521 522 523
    // 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,
    );
    expect(caretOffset.dx, closeTo(SIZE_OF_A * 3, 0.0001));
524
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
525 526 527 528 529
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(SIZE_OF_A * 3, 0.0001));
530 531
    expect(caretOffset.dy, closeTo(0.0, 0.0001));

532
    offset = 4;
533
    painter.text = TextSpan(text: text);
534
    painter.layout();
535 536 537 538
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
539
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
540 541 542 543 544 545 546
    expect(caretOffset.dy, closeTo(SIZE_OF_A, 0.001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(SIZE_OF_A, 0.0001));
547

548 549 550 551 552 553 554 555 556 557 558
    offset = 5;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(SIZE_OF_A * 2, 0.001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
559
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
560
    expect(caretOffset.dy, closeTo(SIZE_OF_A * 2, 0.0001));
561

562 563 564 565 566
    offset = 6;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
567
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
568
    expect(caretOffset.dy, closeTo(SIZE_OF_A * 3, 0.0001));
569

570 571 572 573
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
574
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
575
    expect(caretOffset.dy, closeTo(SIZE_OF_A * 3, 0.0001));
576

577 578 579 580 581 582 583 584 585 586
    // 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,
    );
587
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
588 589 590 591 592 593 594
    expect(caretOffset.dy, closeTo(SIZE_OF_A * 3, 0.0001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(SIZE_OF_A * 3, 0.0001));
595

596 597 598 599 600
    offset = 2;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
601
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
602 603 604 605 606 607 608
    expect(caretOffset.dy, closeTo(SIZE_OF_A * 2, 0.0001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(SIZE_OF_A * 2, 0.0001));
609

610 611 612 613 614
    offset = 1;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
615
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
616 617 618 619 620 621 622
    expect(caretOffset.dy,closeTo(SIZE_OF_A, 0.0001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(SIZE_OF_A, 0.0001));
623

624 625 626 627 628 629 630 631 632 633 634
    offset = 0;
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset),
      ui.Rect.zero,
    );
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
    caretOffset = painter.getOffsetForCaret(
      ui.TextPosition(offset: offset, affinity: TextAffinity.upstream),
      ui.Rect.zero,
    );
635
    expect(caretOffset.dx, closeTo(0.0, 0.0001));
636
    expect(caretOffset.dy, closeTo(0.0, 0.0001));
637
  });
638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662

  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)),
663
      ],
664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 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 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730
    );

    // 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);

    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));
    // line should break here
    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));
731
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/42086
732

733 734 735 736 737 738 739 740 741
  // Null values are valid. See https://github.com/flutter/flutter/pull/48346#issuecomment-584839221
  test('TextPainter set TextHeightBehavior null test', () {
    final TextPainter painter = TextPainter(textHeightBehavior: null)
      ..textDirection = TextDirection.ltr;

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

742 743 744 745 746 747 748 749 750 751 752
  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);

753 754 755
    expect(painter.text, const TextSpan(text: text));
    expect(painter.preferredLineHeight, 14);

756 757 758 759
    final List<ui.LineMetrics> lines = painter.computeLineMetrics();

    expect(lines.length, 4);

760 761 762 763
    // TODO(garyq): This data dump is for debugging a test flake. This should
    // be removed when it is no longer useful.
    if (lines[1].hardBreak == true) {
      print('LineMetrics called: ${lines.length}');
764
      for (final ui.LineMetrics line in lines) {
765 766 767 768
        print('${line.lineNumber}: ${line.hardBreak}');
      }
    }

769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812
    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);
813 814 815 816 817 818

  // Disable this test, this is causing a large amount of flaking and
  // does not have a clear cause. This may or may not be a dart compiler
  // issue or similar. See https://github.com/flutter/flutter/issues/43763
  // for more info.
  }, skip: true);
819 820 821 822 823 824 825 826 827 828 829 830 831 832 833

  test('TextPainter caret height and line height', () {
    final TextPainter painter = TextPainter()
      ..textDirection = TextDirection.ltr
      ..strutStyle = const StrutStyle(fontSize: 50.0);

    const String text = 'A';
    painter.text = const TextSpan(text: text, style: TextStyle(height: 1.0));
    painter.layout();

    final double caretHeight = painter.getFullHeightForCaret(
      const ui.TextPosition(offset: 0),
      ui.Rect.zero,
    );
    expect(caretHeight, 50.0);
834
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/56308
835
}