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

5
import 'dart:ui' as ui show TextBox, BoxHeightStyle, BoxWidthStyle;
6

7
import 'package:flutter/gestures.dart';
8
import 'package:flutter/rendering.dart';
9
import 'package:flutter/widgets.dart';
10
import 'package:flutter_test/flutter_test.dart';
11 12 13

import 'rendering_tester.dart';

14
const String _kText = "I polished up that handle so carefullee\nThat now I am the Ruler of the Queen's Navee!";
15

16 17 18 19 20 21
// A subclass of RenderParagraph that returns an empty list in getBoxesForSelection
// for a given TextSelection.
// This is intended to simulate SkParagraph's implementation of Paragraph.getBoxesForRange,
// which may return an empty list in some situations where Libtxt would return a list
// containing an empty box.
class RenderParagraphWithEmptySelectionBoxList extends RenderParagraph {
22 23 24 25
  RenderParagraphWithEmptySelectionBoxList(
    InlineSpan text, {
    required TextDirection textDirection,
    required this.emptyListSelection,
26 27 28 29 30
  }) : super(text, textDirection: textDirection);

  TextSelection emptyListSelection;

  @override
31 32 33 34 35
  List<ui.TextBox> getBoxesForSelection(
    TextSelection selection, {
    ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
    ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
  }) {
36 37 38
    if (selection == emptyListSelection) {
      return <ui.TextBox>[];
    }
39 40 41 42 43
    return super.getBoxesForSelection(
      selection,
      boxHeightStyle: boxHeightStyle,
      boxWidthStyle: boxWidthStyle,
    );
44 45 46
  }
}

47 48 49 50 51 52 53 54 55 56 57 58
// A subclass of RenderParagraph that returns an empty list in getBoxesForSelection
// for a selection representing a WidgetSpan.
// This is intended to simulate how SkParagraph's implementation of Paragraph.getBoxesForRange
// can return an empty list for a WidgetSpan with empty dimensions.
class RenderParagraphWithEmptyBoxListForWidgetSpan extends RenderParagraph {
  RenderParagraphWithEmptyBoxListForWidgetSpan(
    InlineSpan text, {
    required List<RenderBox> children,
    required TextDirection textDirection,
  }) : super(text, children: children, textDirection: textDirection);

  @override
59 60 61 62 63
  List<ui.TextBox> getBoxesForSelection(
    TextSelection selection, {
    ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
    ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
  }) {
64 65 66
    if (text.getSpanForPosition(selection.base) is WidgetSpan) {
      return <ui.TextBox>[];
    }
67 68 69 70 71
    return super.getBoxesForSelection(
      selection,
      boxHeightStyle: boxHeightStyle,
      boxWidthStyle: boxWidthStyle,
    );
72 73 74
  }
}

75 76
void main() {
  test('getOffsetForCaret control test', () {
77
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
78 79 80
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
81 82
    layout(paragraph);

Dan Field's avatar
Dan Field committed
83
    const Rect caret = Rect.fromLTWH(0.0, 0.0, 2.0, 20.0);
84

85
    final Offset offset5 = paragraph.getOffsetForCaret(const TextPosition(offset: 5), caret);
86 87
    expect(offset5.dx, greaterThan(0.0));

88
    final Offset offset25 = paragraph.getOffsetForCaret(const TextPosition(offset: 25), caret);
89 90
    expect(offset25.dx, greaterThan(offset5.dx));

91
    final Offset offset50 = paragraph.getOffsetForCaret(const TextPosition(offset: 50), caret);
92 93 94
    expect(offset50.dy, greaterThan(offset5.dy));
  });

95 96 97 98 99 100 101
  test('getFullHeightForCaret control test', () {
    final RenderParagraph paragraph = RenderParagraph(
      const TextSpan(text: _kText,style: TextStyle(fontSize: 10.0)),
      textDirection: TextDirection.ltr,
    );
    layout(paragraph);

102
    final double height5 = paragraph.getFullHeightForCaret(const TextPosition(offset: 5))!;
103 104 105
    expect(height5, equals(10.0));
  });

106
  test('getPositionForOffset control test', () {
107
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
108 109 110
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
111 112
    layout(paragraph);

113
    final TextPosition position20 = paragraph.getPositionForOffset(const Offset(20.0, 5.0));
114 115
    expect(position20.offset, greaterThan(0.0));

116
    final TextPosition position40 = paragraph.getPositionForOffset(const Offset(40.0, 5.0));
117 118
    expect(position40.offset, greaterThan(position20.offset));

119
    final TextPosition positionBelow = paragraph.getPositionForOffset(const Offset(5.0, 20.0));
120
    expect(positionBelow.offset, greaterThan(position40.offset));
121
  });
122 123

  test('getBoxesForSelection control test', () {
124
    final RenderParagraph paragraph = RenderParagraph(
125
      const TextSpan(text: _kText, style: TextStyle(fontSize: 10.0)),
Ian Hickson's avatar
Ian Hickson committed
126 127
      textDirection: TextDirection.ltr,
    );
128 129 130
    layout(paragraph);

    List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
131
      const TextSelection(baseOffset: 5, extentOffset: 25),
132 133 134 135 136
    );

    expect(boxes.length, equals(1));

    boxes = paragraph.getBoxesForSelection(
137
      const TextSelection(baseOffset: 25, extentOffset: 50),
138 139
    );

140 141
    expect(boxes.any((ui.TextBox box) => box.left == 250 && box.top == 0), isTrue);
    expect(boxes.any((ui.TextBox box) => box.right == 100 && box.top == 10), isTrue);
142
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61016
143

144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
  test('getBoxesForSelection test with multiple TextSpans and lines', () {
    final RenderParagraph paragraph = RenderParagraph(
      const TextSpan(
        text: 'First ',
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
        children: <InlineSpan>[
          TextSpan(text: 'smallsecond ', style: TextStyle(fontSize: 5.0)),
          TextSpan(text: 'third fourth fifth'),
        ],
      ),
      textDirection: TextDirection.ltr,
    );
    // Do layout with width chosen so that this splits as
    // First smallsecond |
    // third fourth |
    // fifth|
    // The corresponding line widths come out to be:
    // 1st line: 120px wide: 6 chars * 10px plus 12 chars * 5px.
    // 2nd line: 130px wide: 13 chars * 10px.
    // 3rd line: 50px wide.
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 140.0));

    final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
      const TextSelection(baseOffset: 0, extentOffset: 36),
    );

    expect(boxes.length, equals(4));

    // The widths of the boxes should match the calculations above.
    // The heights should all be 10, except for the box for 'smallsecond ',
    // which should have height 5, and be alphabetic baseline-aligned with
    // 'First '. The Ahem font specifies alphabetic baselines at 0.2em above the
    // bottom extent, and 0.8em below the top, so the difference in top
    // alignment becomes (10px * 0.8 - 5px * 0.8) = 4px.

    // 'First ':
    expect(boxes[0], const TextBox.fromLTRBD(0.0, 0.0, 60.0, 10.0, TextDirection.ltr));
    // 'smallsecond ' in size 5:
    expect(boxes[1], const TextBox.fromLTRBD(60.0, 4.0, 120.0, 9.0, TextDirection.ltr));
    // 'third fourth ':
    expect(boxes[2], const TextBox.fromLTRBD(0.0, 10.0, 130.0, 20.0, TextDirection.ltr));
    // 'fifth':
    expect(boxes[3], const TextBox.fromLTRBD(0.0, 20.0, 50.0, 30.0, TextDirection.ltr));
187
  }, skip: !isLinux); // mac typography values can differ https://github.com/flutter/flutter/issues/12357
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230

  test('getBoxesForSelection test with boxHeightStyle and boxWidthStyle set to max', () {
    final RenderParagraph paragraph = RenderParagraph(
      const TextSpan(
        text: 'First ',
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
        children: <InlineSpan>[
          TextSpan(text: 'smallsecond ', style: TextStyle(fontSize: 8.0)),
          TextSpan(text: 'third fourth fifth'),
        ],
      ),
      textDirection: TextDirection.ltr,
    );
    // Do layout with width chosen so that this splits as
    // First smallsecond |
    // third fourth |
    // fifth|
    // The corresponding line widths come out to be:
    // 1st line: 156px wide: 6 chars * 10px plus 12 chars * 8px.
    // 2nd line: 130px wide: 13 chars * 10px.
    // 3rd line: 50px wide.
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 160.0));

    final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
      const TextSelection(baseOffset: 0, extentOffset: 36),
      boxHeightStyle: ui.BoxHeightStyle.max,
      boxWidthStyle: ui.BoxWidthStyle.max,
    );

    expect(boxes.length, equals(5));

    // 'First ':
    expect(boxes[0], const TextBox.fromLTRBD(0.0, 0.0, 60.0, 10.0, TextDirection.ltr));
    // 'smallsecond ' in size 8, but on same line as previous box, so height remains 10:
    expect(boxes[1], const TextBox.fromLTRBD(60.0, 0.0, 156.0, 10.0, TextDirection.ltr));
    // 'third fourth ':
    expect(boxes[2], const TextBox.fromLTRBD(0.0, 10.0, 130.0, 20.0, TextDirection.ltr));
    // extra box added to extend width, as per definition of ui.BoxWidthStyle.max:
    expect(boxes[3], const TextBox.fromLTRBD(130.0, 10.0, 156.0, 20.0, TextDirection.ltr));
    // 'fifth':
    expect(boxes[4], const TextBox.fromLTRBD(0.0, 20.0, 50.0, 30.0, TextDirection.ltr));
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61016

231
  test('getWordBoundary control test', () {
232
    final RenderParagraph paragraph = RenderParagraph(
Ian Hickson's avatar
Ian Hickson committed
233 234 235
      const TextSpan(text: _kText),
      textDirection: TextDirection.ltr,
    );
236 237
    layout(paragraph);

238
    final TextRange range5 = paragraph.getWordBoundary(const TextPosition(offset: 5));
239 240
    expect(range5.textInside(_kText), equals('polished'));

241
    final TextRange range50 = paragraph.getWordBoundary(const TextPosition(offset: 50));
242 243
    expect(range50.textInside(_kText), equals(' '));

244
    final TextRange range85 = paragraph.getWordBoundary(const TextPosition(offset: 75));
245
    expect(range85.textInside(_kText), equals("Queen's"));
246
  });
247 248

  test('overflow test', () {
249
    final RenderParagraph paragraph = RenderParagraph(
250 251 252
      const TextSpan(
        text: 'This\n' // 4 characters * 10px font size = 40px width on the first line
              'is a wrapping test. It should wrap at manual newlines, and if softWrap is true, also at spaces.',
253
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
254
      ),
Ian Hickson's avatar
Ian Hickson committed
255
      textDirection: TextDirection.ltr,
256 257 258 259
      maxLines: 1,
      softWrap: true,
    );

260 261 262 263 264
    void relayoutWith({
      int? maxLines,
      required bool softWrap,
      required TextOverflow overflow,
    }) {
265 266 267 268 269 270 271 272
      paragraph
        ..maxLines = maxLines
        ..softWrap = softWrap
        ..overflow = overflow;
      pumpFrame();
    }

    // Lay out in a narrow box to force wrapping.
273
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0)); // enough to fit "This" but not "This is"
274
    final double lineHeight = paragraph.size.height;
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310

    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.clip);
    expect(paragraph.size.height, equals(3 * lineHeight));

    relayoutWith(maxLines: null, softWrap: true, overflow: TextOverflow.clip);
    expect(paragraph.size.height, greaterThan(5 * lineHeight));

    // Try again with ellipsis overflow. We can't test that the ellipsis are
    // drawn, but we can test the sizing.
    relayoutWith(maxLines: 1, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(lineHeight));

    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(3 * lineHeight));

    // This is the one weird case. If maxLines is null, we would expect to allow
    // infinite wrapping. However, if we did, we'd never know when to append an
    // ellipsis, so this really means "append ellipsis as soon as we exceed the
    // width".
    relayoutWith(maxLines: null, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(2 * lineHeight));

    // Now with no soft wrapping.
    relayoutWith(maxLines: 1, softWrap: false, overflow: TextOverflow.clip);
    expect(paragraph.size.height, equals(lineHeight));

    relayoutWith(maxLines: 3, softWrap: false, overflow: TextOverflow.clip);
    expect(paragraph.size.height, equals(2 * lineHeight));

    relayoutWith(maxLines: null, softWrap: false, overflow: TextOverflow.clip);
    expect(paragraph.size.height, equals(2 * lineHeight));

    relayoutWith(maxLines: 1, softWrap: false, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(lineHeight));

    relayoutWith(maxLines: 3, softWrap: false, overflow: TextOverflow.ellipsis);
311
    expect(paragraph.size.height, equals(3 * lineHeight));
312 313 314

    relayoutWith(maxLines: null, softWrap: false, overflow: TextOverflow.ellipsis);
    expect(paragraph.size.height, equals(2 * lineHeight));
315 316 317 318 319

    // Test presence of the fade effect.
    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.fade);
    expect(paragraph.debugHasOverflowShader, isTrue);

320 321 322 323
    // Change back to ellipsis and check that the fade shader is cleared.
    relayoutWith(maxLines: 3, softWrap: true, overflow: TextOverflow.ellipsis);
    expect(paragraph.debugHasOverflowShader, isFalse);

324 325
    relayoutWith(maxLines: 100, softWrap: true, overflow: TextOverflow.fade);
    expect(paragraph.debugHasOverflowShader, isFalse);
326
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61018
327 328

  test('maxLines', () {
329
    final RenderParagraph paragraph = RenderParagraph(
330
      const TextSpan(
331
        text: "How do you write like you're running out of time? Write day and night like you're running out of time?",
332 333
            // 0123456789 0123456789 012 345 0123456 012345 01234 012345678 012345678 0123 012 345 0123456 012345 01234
            // 0          1          2       3       4      5     6         7         8    9       10      11     12
334
        style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
335
      ),
Ian Hickson's avatar
Ian Hickson committed
336
      textDirection: TextDirection.ltr,
337 338
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));
339
    void layoutAt(int? maxLines) {
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
      paragraph.maxLines = maxLines;
      pumpFrame();
    }

    layoutAt(null);
    expect(paragraph.size.height, 130.0);

    layoutAt(1);
    expect(paragraph.size.height, 10.0);

    layoutAt(2);
    expect(paragraph.size.height, 20.0);

    layoutAt(3);
    expect(paragraph.size.height, 30.0);
355
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61018
356

357
  test('changing color does not do layout', () {
358
    final RenderParagraph paragraph = RenderParagraph(
359 360
      const TextSpan(
        text: 'Hello',
361
        style: TextStyle(color: Color(0xFF000000)),
362
      ),
Ian Hickson's avatar
Ian Hickson committed
363
      textDirection: TextDirection.ltr,
364 365 366 367 368 369
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0), phase: EnginePhase.paint);
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isFalse);
    paragraph.text = const TextSpan(
      text: 'Hello World',
370
      style: TextStyle(color: Color(0xFF000000)),
371 372 373 374 375 376 377 378
    );
    expect(paragraph.debugNeedsLayout, isTrue);
    expect(paragraph.debugNeedsPaint, isFalse);
    pumpFrame(phase: EnginePhase.paint);
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isFalse);
    paragraph.text = const TextSpan(
      text: 'Hello World',
379
      style: TextStyle(color: Color(0xFFFFFFFF)),
380 381 382 383 384 385 386 387
    );
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isTrue);
    pumpFrame(phase: EnginePhase.paint);
    expect(paragraph.debugNeedsLayout, isFalse);
    expect(paragraph.debugNeedsPaint, isFalse);
  });

388
  test('nested TextSpans in paragraph handle textScaleFactor correctly.', () {
389
    const TextSpan testSpan = TextSpan(
390
      text: 'a',
391
      style: TextStyle(
392 393
        fontSize: 10.0,
      ),
394 395
      children: <TextSpan>[
        TextSpan(
396
          text: 'b',
397 398
          children: <TextSpan>[
            TextSpan(text: 'c'),
399
          ],
400
          style: TextStyle(
401 402 403
            fontSize: 20.0,
          ),
        ),
404
        TextSpan(
405 406 407 408
          text: 'd',
        ),
      ],
    );
409
    final RenderParagraph paragraph = RenderParagraph(
410 411
        testSpan,
        textDirection: TextDirection.ltr,
412
        textScaleFactor: 1.3,
413 414 415 416
    );
    paragraph.layout(const BoxConstraints());
    // anyOf is needed here because Linux and Mac have different text
    // rendering widths in tests.
417
    // TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
418 419 420 421 422
    expect(paragraph.size.width, anyOf(79.0, 78.0));
    expect(paragraph.size.height, 26.0);

    // Test the sizes of nested spans.
    final String text = testSpan.toStringDeep();
423 424 425
    final List<ui.TextBox> boxes = <ui.TextBox>[
      for (int i = 0; i < text.length; ++i)
        ...paragraph.getBoxesForSelection(
426
          TextSelection(baseOffset: i, extentOffset: i + 1),
427 428
        ),
    ];
429 430 431 432
    expect(boxes.length, equals(4));

    // anyOf is needed here and below because Linux and Mac have different text
    // rendering widths in tests.
433
    // TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
434
    expect(boxes[0].toRect().width, anyOf(14.0, 13.0));
435
    expect(boxes[0].toRect().height, moreOrLessEquals(13.0, epsilon: 0.0001));
436
    expect(boxes[1].toRect().width, anyOf(27.0, 26.0));
437
    expect(boxes[1].toRect().height, moreOrLessEquals(26.0, epsilon: 0.0001));
438
    expect(boxes[2].toRect().width, anyOf(27.0, 26.0));
439
    expect(boxes[2].toRect().height, moreOrLessEquals(26.0, epsilon: 0.0001));
440
    expect(boxes[3].toRect().width, anyOf(14.0, 13.0));
441
    expect(boxes[3].toRect().height, moreOrLessEquals(13.0, epsilon: 0.0001));
442
  });
443

444
  test('toStringDeep', () {
445
    final RenderParagraph paragraph = RenderParagraph(
446
      const TextSpan(text: _kText),
Ian Hickson's avatar
Ian Hickson committed
447
      textDirection: TextDirection.ltr,
448
      locale: const Locale('ja', 'JP'),
449
    );
450
    expect(paragraph, hasAGoodToStringDeep);
451
    expect(
452
      paragraph.toStringDeep(minLevel: DiagnosticLevel.info),
453 454
      equalsIgnoringHashCodes(
        'RenderParagraph#00000 NEEDS-LAYOUT NEEDS-PAINT DETACHED\n'
Ian Hickson's avatar
Ian Hickson committed
455 456
        ' │ parentData: MISSING\n'
        ' │ constraints: MISSING\n'
457
        ' │ size: MISSING\n'
Ian Hickson's avatar
Ian Hickson committed
458 459 460 461
        ' │ textAlign: start\n'
        ' │ textDirection: ltr\n'
        ' │ softWrap: wrapping at box width\n'
        ' │ overflow: clip\n'
462
        ' │ locale: ja_JP\n'
Ian Hickson's avatar
Ian Hickson committed
463
        ' │ maxLines: unlimited\n'
464 465 466
        ' ╘═╦══ text ═══\n'
        '   ║ TextSpan:\n'
        '   ║   "I polished up that handle so carefullee\n'
467
        '   ║   That now I am the Ruler of the Queen\'s Navee!"\n'
468
        '   ╚═══════════\n',
469 470 471
      ),
    );
  });
472 473 474 475

  test('locale setter', () {
    // Regression test for https://github.com/flutter/flutter/issues/18175

476
    final RenderParagraph paragraph = RenderParagraph(
477 478 479 480 481 482 483 484 485 486
      const TextSpan(text: _kText),
      locale: const Locale('zh', 'HK'),
      textDirection: TextDirection.ltr,
    );
    expect(paragraph.locale, const Locale('zh', 'HK'));

    paragraph.locale = const Locale('ja', 'JP');
    expect(paragraph.locale, const Locale('ja', 'JP'));
  });

487 488 489 490 491 492 493 494 495 496 497 498 499
  test('inline widgets test', () {
    const TextSpan text = TextSpan(
      text: 'a',
      style: TextStyle(fontSize: 10.0),
      children: <InlineSpan>[
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        TextSpan(text: 'a'),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
      ],
    );
    // Fake the render boxes that correspond to the WidgetSpans. We use
    // RenderParagraph to reduce dependencies this test has.
500 501 502 503 504
    final List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
    ];
505 506 507 508 509 510 511 512 513

    final RenderParagraph paragraph = RenderParagraph(
      text,
      textDirection: TextDirection.ltr,
      children: renderBoxes,
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));

    final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
514
      const TextSelection(baseOffset: 0, extentOffset: 8),
515 516 517 518 519 520 521 522
    );

    expect(boxes.length, equals(5));
    expect(boxes[0], const TextBox.fromLTRBD(0.0, 4.0, 10.0, 14.0, TextDirection.ltr));
    expect(boxes[1], const TextBox.fromLTRBD(10.0, 0.0, 24.0, 14.0, TextDirection.ltr));
    expect(boxes[2], const TextBox.fromLTRBD(24.0, 0.0, 38.0, 14.0, TextDirection.ltr));
    expect(boxes[3], const TextBox.fromLTRBD(38.0, 4.0, 48.0, 14.0, TextDirection.ltr));
    expect(boxes[4], const TextBox.fromLTRBD(48.0, 0.0, 62.0, 14.0, TextDirection.ltr));
523
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
524

525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565
  test('getBoxesForSelection with boxHeightStyle for inline widgets', () {
    const TextSpan text = TextSpan(
      text: 'a',
      style: TextStyle(fontSize: 10.0),
      children: <InlineSpan>[
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        TextSpan(text: 'a'),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
      ],
    );
    // Fake the render boxes that correspond to the WidgetSpans. We use
    // RenderParagraph to reduce the dependencies this test has. The dimensions
    // of these get used in place of the widths and heights specified in the
    // SizedBoxes above: each comes out as (w,h) = (14,14).
    final List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
    ];

    final RenderParagraph paragraph = RenderParagraph(
      text,
      textDirection: TextDirection.ltr,
      children: renderBoxes,
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));

    final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
      const TextSelection(baseOffset: 0, extentOffset: 8),
      boxHeightStyle: ui.BoxHeightStyle.max,
    );

    expect(boxes.length, equals(5));
    expect(boxes[0], const TextBox.fromLTRBD(0.0, 0.0, 10.0, 14.0, TextDirection.ltr));
    expect(boxes[1], const TextBox.fromLTRBD(10.0, 0.0, 24.0, 14.0, TextDirection.ltr));
    expect(boxes[2], const TextBox.fromLTRBD(24.0, 0.0, 38.0, 14.0, TextDirection.ltr));
    expect(boxes[3], const TextBox.fromLTRBD(38.0, 0.0, 48.0, 14.0, TextDirection.ltr));
    expect(boxes[4], const TextBox.fromLTRBD(48.0, 0.0, 62.0, 14.0, TextDirection.ltr));
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020

566 567 568 569 570 571 572 573 574 575
  test('can compute IntrinsicHeight for widget span', () {
    // Regression test for https://github.com/flutter/flutter/issues/59316
    const double screenWidth = 100.0;
    const String sentence = 'one two';
    List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: sentence), textDirection: TextDirection.ltr),
    ];
    RenderParagraph paragraph = RenderParagraph(
      const TextSpan(
        children: <InlineSpan> [
576 577
          WidgetSpan(child: Text(sentence)),
        ],
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593
      ),
      textScaleFactor: 1.0,
      children: renderBoxes,
      textDirection: TextDirection.ltr,
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
    final double singleLineHeight = paragraph.computeMaxIntrinsicHeight(screenWidth);
    expect(singleLineHeight, 14.0);

    pumpFrame();
    renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: sentence), textDirection: TextDirection.ltr),
    ];
    paragraph = RenderParagraph(
      const TextSpan(
        children: <InlineSpan> [
594 595
          WidgetSpan(child: Text(sentence)),
        ],
596 597 598 599 600 601 602 603 604 605 606 607
      ),
      textScaleFactor: 2.0,
      children: renderBoxes,
      textDirection: TextDirection.ltr,
    );

    layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
    final double maxIntrinsicHeight = paragraph.computeMaxIntrinsicHeight(screenWidth);
    final double minIntrinsicHeight = paragraph.computeMinIntrinsicHeight(screenWidth);
    // intrinsicHeight = singleLineHeight * textScaleFactor * two lines.
    expect(maxIntrinsicHeight, singleLineHeight * 2.0 * 2);
    expect(maxIntrinsicHeight, minIntrinsicHeight);
608
  });
609 610 611 612 613 614 615 616 617 618 619 620

  test('can compute IntrinsicWidth for widget span', () {
    // Regression test for https://github.com/flutter/flutter/issues/59316
    const double screenWidth = 1000.0;
    const double fixedHeight = 1000.0;
    const String sentence = 'one two';
    List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: sentence), textDirection: TextDirection.ltr),
    ];
    RenderParagraph paragraph = RenderParagraph(
      const TextSpan(
        children: <InlineSpan> [
621 622
          WidgetSpan(child: Text(sentence)),
        ],
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638
      ),
      textScaleFactor: 1.0,
      children: renderBoxes,
      textDirection: TextDirection.ltr,
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
    final double widthForOneLine = paragraph.computeMaxIntrinsicWidth(fixedHeight);
    expect(widthForOneLine, 98.0);

    pumpFrame();
    renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: sentence), textDirection: TextDirection.ltr),
    ];
    paragraph = RenderParagraph(
      const TextSpan(
        children: <InlineSpan> [
639 640
          WidgetSpan(child: Text(sentence)),
        ],
641 642 643 644 645 646 647 648 649 650
      ),
      textScaleFactor: 2.0,
      children: renderBoxes,
      textDirection: TextDirection.ltr,
    );

    layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
    final double maxIntrinsicWidth = paragraph.computeMaxIntrinsicWidth(fixedHeight);
    // maxIntrinsicWidth = widthForOneLine * textScaleFactor
    expect(maxIntrinsicWidth, widthForOneLine * 2.0);
651
  });
652

653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669
  test('inline widgets multiline test', () {
    const TextSpan text = TextSpan(
      text: 'a',
      style: TextStyle(fontSize: 10.0),
      children: <InlineSpan>[
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        TextSpan(text: 'a'),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
        WidgetSpan(child: SizedBox(width: 21, height: 21)),
      ],
    );
    // Fake the render boxes that correspond to the WidgetSpans. We use
    // RenderParagraph to reduce dependencies this test has.
670 671 672 673 674 675 676 677 678
    final List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
    ];
679 680 681 682 683 684 685 686 687

    final RenderParagraph paragraph = RenderParagraph(
      text,
      textDirection: TextDirection.ltr,
      children: renderBoxes,
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0));

    final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
688
      const TextSelection(baseOffset: 0, extentOffset: 12),
689 690 691 692 693 694 695 696 697 698 699 700 701 702
    );

    expect(boxes.length, equals(9));
    expect(boxes[0], const TextBox.fromLTRBD(0.0, 4.0, 10.0, 14.0, TextDirection.ltr));
    expect(boxes[1], const TextBox.fromLTRBD(10.0, 0.0, 24.0, 14.0, TextDirection.ltr));
    expect(boxes[2], const TextBox.fromLTRBD(24.0, 0.0, 38.0, 14.0, TextDirection.ltr));
    expect(boxes[3], const TextBox.fromLTRBD(38.0, 4.0, 48.0, 14.0, TextDirection.ltr));
    // Wraps
    expect(boxes[4], const TextBox.fromLTRBD(0.0, 14.0, 14.0, 28.0 , TextDirection.ltr));
    expect(boxes[5], const TextBox.fromLTRBD(14.0, 14.0, 28.0, 28.0, TextDirection.ltr));
    expect(boxes[6], const TextBox.fromLTRBD(28.0, 14.0, 42.0, 28.0, TextDirection.ltr));
    // Wraps
    expect(boxes[7], const TextBox.fromLTRBD(0.0, 28.0, 14.0, 42.0, TextDirection.ltr));
    expect(boxes[8], const TextBox.fromLTRBD(14.0, 28.0, 28.0, 42.0 , TextDirection.ltr));
703
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
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 731 732 733 734 735 736 737 738
  test('Does not include the semantics node of truncated rendering children', () {
    // Regression test for https://github.com/flutter/flutter/issues/88180
    const double screenWidth = 100;
    const String sentence = 'truncated';
    final List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(
          const TextSpan(text: sentence), textDirection: TextDirection.ltr),
    ];
    final RenderParagraph paragraph = RenderParagraph(
      const TextSpan(
        text: 'a long line to be truncated.',
        children: <InlineSpan>[
          WidgetSpan(child: Text(sentence)),
        ],
      ),
      overflow: TextOverflow.ellipsis,
      textScaleFactor: 1.0,
      children: renderBoxes,
      textDirection: TextDirection.ltr,
    );
    layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
    final SemanticsNode result = SemanticsNode();
    final SemanticsNode truncatedChild = SemanticsNode();
    truncatedChild.tags = <SemanticsTag>{const PlaceholderSpanIndexSemanticsTag(0)};
    paragraph.assembleSemanticsNode(result, SemanticsConfiguration(), <SemanticsNode>[truncatedChild]);
    // It should only contain the semantics node of the TextSpan.
    expect(result.childrenCount, 1);
    result.visitChildren((SemanticsNode node) {
      expect(node != truncatedChild, isTrue);
      return true;
    });
  });

    test('Supports gesture recognizer semantics', () {
739 740 741 742 743 744 745 746 747 748 749
    final RenderParagraph paragraph = RenderParagraph(
      TextSpan(text: _kText, children: <InlineSpan>[
        TextSpan(text: 'one', recognizer: TapGestureRecognizer()..onTap = () {}),
        TextSpan(text: 'two', recognizer: LongPressGestureRecognizer()..onLongPress = () {}),
        TextSpan(text: 'three', recognizer: DoubleTapGestureRecognizer()..onDoubleTap = () {}),
      ]),
      textDirection: TextDirection.rtl,
    );
    layout(paragraph);

    paragraph.assembleSemanticsNode(SemanticsNode(), SemanticsConfiguration(), <SemanticsNode>[]);
750
  });
751

752 753 754 755 756 757 758 759 760 761 762 763
  test('Supports empty text span with spell out', () {
    final RenderParagraph paragraph = RenderParagraph(
      const TextSpan(text: '', spellOut: true),
      textDirection: TextDirection.rtl,
    );
    layout(paragraph);
    final SemanticsNode node = SemanticsNode();
    paragraph.assembleSemanticsNode(node, SemanticsConfiguration(), <SemanticsNode>[]);
    expect(node.attributedLabel.string, '');
    expect(node.attributedLabel.attributes.length, 0);
  });

764 765 766 767 768 769 770 771 772 773 774 775
  test('Asserts on unsupported gesture recognizer', () {
    final RenderParagraph paragraph = RenderParagraph(
      TextSpan(text: _kText, children: <InlineSpan>[
        TextSpan(text: 'three', recognizer: MultiTapGestureRecognizer()..onTap = (int id) {}),
      ]),
      textDirection: TextDirection.rtl,
    );
    layout(paragraph);

    bool failed = false;
    try {
      paragraph.assembleSemanticsNode(SemanticsNode(), SemanticsConfiguration(), <SemanticsNode>[]);
776
    } on AssertionError catch (e) {
777 778 779 780
      failed = true;
      expect(e.message, 'MultiTapGestureRecognizer is not supported.');
    }
    expect(failed, true);
781
  });
782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797

  test('assembleSemanticsNode handles text spans that do not yield selection boxes', () {
    final RenderParagraph paragraph = RenderParagraphWithEmptySelectionBoxList(
      TextSpan(text: '', children: <InlineSpan>[
        TextSpan(text: 'A', recognizer: TapGestureRecognizer()..onTap = () {}),
        TextSpan(text: 'B', recognizer: TapGestureRecognizer()..onTap = () {}),
        TextSpan(text: 'C', recognizer: TapGestureRecognizer()..onTap = () {}),
      ]),
      textDirection: TextDirection.rtl,
      emptyListSelection: const TextSelection(baseOffset: 0, extentOffset: 1),
    );
    layout(paragraph);

    final SemanticsNode node = SemanticsNode();
    paragraph.assembleSemanticsNode(node, SemanticsConfiguration(), <SemanticsNode>[]);
    expect(node.childrenCount, 2);
798
  });
799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819

  test('assembleSemanticsNode handles empty WidgetSpans that do not yield selection boxes', () {
    final TextSpan text = TextSpan(text: '', children: <InlineSpan>[
      TextSpan(text: 'A', recognizer: TapGestureRecognizer()..onTap = () {}),
      const WidgetSpan(child: SizedBox(width: 0, height: 0)),
      TextSpan(text: 'C', recognizer: TapGestureRecognizer()..onTap = () {}),
    ]);
    final List<RenderBox> renderBoxes = <RenderBox>[
      RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
    ];
    final RenderParagraph paragraph = RenderParagraphWithEmptyBoxListForWidgetSpan(
      text,
      children: renderBoxes,
      textDirection: TextDirection.ltr,
    );
    layout(paragraph);

    final SemanticsNode node = SemanticsNode();
    paragraph.assembleSemanticsNode(node, SemanticsConfiguration(), <SemanticsNode>[]);
    expect(node.childrenCount, 2);
  }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
820
}