paragraph.dart 36.3 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:collection';
6
import 'dart:math' as math;
7
import 'dart:ui' as ui show Gradient, Shader, TextBox, PlaceholderAlignment, TextHeightBehavior;
8

9
import 'package:flutter/foundation.dart';
10
import 'package:flutter/gestures.dart';
11
import 'package:flutter/semantics.dart';
12

13
import 'package:vector_math/vector_math_64.dart';
14

15
import 'box.dart';
16
import 'debug.dart';
17
import 'object.dart';
18

19 20
const String _kEllipsis = '\u2026';

21 22 23
/// Parent data for use with [RenderParagraph].
class TextParentData extends ContainerBoxParentData<RenderBox> {
  /// The scaling of the text.
24
  double? scale;
25 26 27

  @override
  String toString() {
28
    final List<String> values = <String>[
29
      'offset=$offset',
30 31 32
      if (scale != null) 'scale=$scale',
      super.toString(),
    ];
33 34 35 36
    return values.join('; ');
  }
}

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
/// Used by the [RenderParagraph] to map its rendering children to their
/// corresponding semantics nodes.
///
/// The [RichText] uses this to tag the relation between its placeholder spans
/// and their semantics nodes.
@immutable
class PlaceholderSpanIndexSemanticsTag extends SemanticsTag {
  /// Creates a semantics tag with the input `index`.
  ///
  /// Different [PlaceholderSpanIndexSemanticsTag]s with the same `index` are
  /// consider the same.
  const PlaceholderSpanIndexSemanticsTag(this.index) : super('PlaceholderSpanIndexSemanticsTag($index)');

  /// The index of this tag.
  final int index;

  @override
  bool operator ==(Object other) {
    return other is PlaceholderSpanIndexSemanticsTag
        && other.index == index;
  }

  @override
  int get hashCode => hashValues(PlaceholderSpanIndexSemanticsTag, index);
}

63 64 65
/// A render object that displays a paragraph of text.
class RenderParagraph extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, TextParentData>,
66 67
             RenderBoxContainerDefaultsMixin<RenderBox, TextParentData>,
                  RelayoutWhenSystemFontsChangeMixin {
68 69
  /// Creates a paragraph render object.
  ///
Ian Hickson's avatar
Ian Hickson committed
70 71
  /// The [text], [textAlign], [textDirection], [overflow], [softWrap], and
  /// [textScaleFactor] arguments must not be null.
72 73 74
  ///
  /// The [maxLines] property may be null (and indeed defaults to null), but if
  /// it is not null, it must be greater than zero.
75
  RenderParagraph(InlineSpan text, {
76
    TextAlign textAlign = TextAlign.start,
77
    required TextDirection textDirection,
78 79 80
    bool softWrap = true,
    TextOverflow overflow = TextOverflow.clip,
    double textScaleFactor = 1.0,
81 82 83
    int? maxLines,
    Locale? locale,
    StrutStyle? strutStyle,
84
    TextWidthBasis textWidthBasis = TextWidthBasis.parent,
85 86
    ui.TextHeightBehavior? textHeightBehavior,
    List<RenderBox>? children,
87 88
  }) : assert(text != null),
       assert(text.debugAssertIsValid()),
Ian Hickson's avatar
Ian Hickson committed
89 90
       assert(textAlign != null),
       assert(textDirection != null),
91 92 93
       assert(softWrap != null),
       assert(overflow != null),
       assert(textScaleFactor != null),
94
       assert(maxLines == null || maxLines > 0),
95
       assert(textWidthBasis != null),
96
       _softWrap = softWrap,
97
       _overflow = overflow,
98
       _textPainter = TextPainter(
99 100
         text: text,
         textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
101
         textDirection: textDirection,
102 103 104
         textScaleFactor: textScaleFactor,
         maxLines: maxLines,
         ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
105
         locale: locale,
106
         strutStyle: strutStyle,
107
         textWidthBasis: textWidthBasis,
108
         textHeightBehavior: textHeightBehavior,
109 110 111 112 113 114 115 116 117 118
       ) {
    addAll(children);
    _extractPlaceholderSpans(text);
  }

  @override
  void setupParentData(RenderBox child) {
    if (child.parentData is! TextParentData)
      child.parentData = TextParentData();
  }
119

120
  final TextPainter _textPainter;
121

122
  /// The text to display.
123
  InlineSpan get text => _textPainter.text!;
124
  set text(InlineSpan value) {
125
    assert(value != null);
126
    switch (_textPainter.text!.compareTo(value)) {
127 128 129 130 131
      case RenderComparison.identical:
      case RenderComparison.metadata:
        return;
      case RenderComparison.paint:
        _textPainter.text = value;
132
        _extractPlaceholderSpans(value);
133
        markNeedsPaint();
134
        markNeedsSemanticsUpdate();
135 136 137 138
        break;
      case RenderComparison.layout:
        _textPainter.text = value;
        _overflowShader = null;
139
        _extractPlaceholderSpans(value);
140 141 142
        markNeedsLayout();
        break;
    }
143 144
  }

145
  late List<PlaceholderSpan> _placeholderSpans;
146 147 148 149
  void _extractPlaceholderSpans(InlineSpan span) {
    _placeholderSpans = <PlaceholderSpan>[];
    span.visitChildren((InlineSpan span) {
      if (span is PlaceholderSpan) {
150
        _placeholderSpans.add(span);
151 152 153 154 155
      }
      return true;
    });
  }

156 157
  /// How the text should be aligned horizontally.
  TextAlign get textAlign => _textPainter.textAlign;
158
  set textAlign(TextAlign value) {
Ian Hickson's avatar
Ian Hickson committed
159
    assert(value != null);
160 161 162 163 164 165
    if (_textPainter.textAlign == value)
      return;
    _textPainter.textAlign = value;
    markNeedsPaint();
  }

Ian Hickson's avatar
Ian Hickson committed
166 167 168 169 170 171 172 173 174
  /// The directionality of the text.
  ///
  /// This decides how the [TextAlign.start], [TextAlign.end], and
  /// [TextAlign.justify] values of [textAlign] are interpreted.
  ///
  /// This is also used to disambiguate how to render bidirectional text. For
  /// example, if the [text] is an English phrase followed by a Hebrew phrase,
  /// in a [TextDirection.ltr] context the English phrase will be on the left
  /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
175
  /// context, the English phrase will be on the right and the Hebrew phrase on
Ian Hickson's avatar
Ian Hickson committed
176 177 178
  /// its left.
  ///
  /// This must not be null.
179
  TextDirection get textDirection => _textPainter.textDirection!;
Ian Hickson's avatar
Ian Hickson committed
180 181 182 183 184 185 186 187
  set textDirection(TextDirection value) {
    assert(value != null);
    if (_textPainter.textDirection == value)
      return;
    _textPainter.textDirection = value;
    markNeedsLayout();
  }

188 189
  /// Whether the text should break at soft line breaks.
  ///
190 191 192 193 194
  /// If false, the glyphs in the text will be positioned as if there was
  /// unlimited horizontal space.
  ///
  /// If [softWrap] is false, [overflow] and [textAlign] may have unexpected
  /// effects.
195 196
  bool get softWrap => _softWrap;
  bool _softWrap;
197
  set softWrap(bool value) {
198 199 200 201 202 203 204 205 206 207
    assert(value != null);
    if (_softWrap == value)
      return;
    _softWrap = value;
    markNeedsLayout();
  }

  /// How visual overflow should be handled.
  TextOverflow get overflow => _overflow;
  TextOverflow _overflow;
208
  set overflow(TextOverflow value) {
209 210 211 212
    assert(value != null);
    if (_overflow == value)
      return;
    _overflow = value;
213
    _textPainter.ellipsis = value == TextOverflow.ellipsis ? _kEllipsis : null;
214
    markNeedsLayout();
215 216
  }

217 218 219 220 221 222 223 224 225 226 227 228 229 230
  /// The number of font pixels for each logical pixel.
  ///
  /// For example, if the text scale factor is 1.5, text will be 50% larger than
  /// the specified font size.
  double get textScaleFactor => _textPainter.textScaleFactor;
  set textScaleFactor(double value) {
    assert(value != null);
    if (_textPainter.textScaleFactor == value)
      return;
    _textPainter.textScaleFactor = value;
    _overflowShader = null;
    markNeedsLayout();
  }

231 232 233
  /// An optional maximum number of lines for the text to span, wrapping if
  /// necessary. If the text exceeds the given number of lines, it will be
  /// truncated according to [overflow] and [softWrap].
234
  int? get maxLines => _textPainter.maxLines;
235 236
  /// The value may be null. If it is not null, then it must be greater than
  /// zero.
237
  set maxLines(int? value) {
238
    assert(value == null || value > 0);
239 240 241 242 243 244 245
    if (_textPainter.maxLines == value)
      return;
    _textPainter.maxLines = value;
    _overflowShader = null;
    markNeedsLayout();
  }

246 247
  /// Used by this paragraph's internal [TextPainter] to select a
  /// locale-specific font.
248
  ///
249 250 251 252
  /// In some cases, the same Unicode character may be rendered differently
  /// depending on the locale. For example, the '骨' character is rendered
  /// differently in the Chinese and Japanese locales. In these cases, the
  /// [locale] may be used to select a locale-specific font.
253
  Locale? get locale => _textPainter.locale;
254
  /// The value may be null.
255
  set locale(Locale? value) {
256 257
    if (_textPainter.locale == value)
      return;
258
    _textPainter.locale = value;
259 260 261 262
    _overflowShader = null;
    markNeedsLayout();
  }

263
  /// {@macro flutter.painting.textPainter.strutStyle}
264
  StrutStyle? get strutStyle => _textPainter.strutStyle;
265
  /// The value may be null.
266
  set strutStyle(StrutStyle? value) {
267 268 269 270 271 272 273
    if (_textPainter.strutStyle == value)
      return;
    _textPainter.strutStyle = value;
    _overflowShader = null;
    markNeedsLayout();
  }

274
  /// {@macro flutter.painting.textPainter.textWidthBasis}
275 276 277 278 279 280 281 282 283 284
  TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis;
  set textWidthBasis(TextWidthBasis value) {
    assert(value != null);
    if (_textPainter.textWidthBasis == value)
      return;
    _textPainter.textWidthBasis = value;
    _overflowShader = null;
    markNeedsLayout();
  }

285
  /// {@macro flutter.dart:ui.textHeightBehavior}
286 287
  ui.TextHeightBehavior? get textHeightBehavior => _textPainter.textHeightBehavior;
  set textHeightBehavior(ui.TextHeightBehavior? value) {
288 289 290 291 292 293 294
    if (_textPainter.textHeightBehavior == value)
      return;
    _textPainter.textHeightBehavior = value;
    _overflowShader = null;
    markNeedsLayout();
  }

295
  @override
296
  double computeMinIntrinsicWidth(double height) {
297 298 299 300 301
    if (!_canComputeIntrinsics()) {
      return 0.0;
    }
    _computeChildrenWidthWithMinIntrinsics(height);
    _layoutText(); // layout with infinite width.
302
    return _textPainter.minIntrinsicWidth;
303 304
  }

305
  @override
306
  double computeMaxIntrinsicWidth(double height) {
307 308 309 310 311
    if (!_canComputeIntrinsics()) {
      return 0.0;
    }
    _computeChildrenWidthWithMaxIntrinsics(height);
    _layoutText(); // layout with infinite width.
312
    return _textPainter.maxIntrinsicWidth;
313 314
  }

315
  double _computeIntrinsicHeight(double width) {
316 317 318 319
    if (!_canComputeIntrinsics()) {
      return 0.0;
    }
    _computeChildrenHeightWithMinIntrinsics(width);
320 321
    _layoutText(minWidth: width, maxWidth: width);
    return _textPainter.height;
322 323
  }

324
  @override
325 326
  double computeMinIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
327 328
  }

329
  @override
330 331
  double computeMaxIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
332 333
  }

334
  @override
335
  double computeDistanceToActualBaseline(TextBaseline baseline) {
336
    assert(!debugNeedsLayout);
337 338
    assert(constraints != null);
    assert(constraints.debugAssertIsValid());
339
    _layoutTextWithConstraints(constraints);
340 341 342 343 344 345
    // TODO(garyq): Since our metric for ideographic baseline is currently
    // inaccurate and the non-alphabetic baselines are based off of the
    // alphabetic baseline, we use the alphabetic for now to produce correct
    // layouts. We should eventually change this back to pass the `baseline`
    // property when the ideographic baseline is properly implemented
    // (https://github.com/flutter/flutter/issues/22625).
346
    return _textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic);
347 348
  }

349 350 351 352
  // Intrinsics cannot be calculated without a full layout for
  // alignments that require the baseline (baseline, aboveBaseline,
  // belowBaseline).
  bool _canComputeIntrinsics() {
353
    for (final PlaceholderSpan span in _placeholderSpans) {
354 355 356 357
      switch (span.alignment) {
        case ui.PlaceholderAlignment.baseline:
        case ui.PlaceholderAlignment.aboveBaseline:
        case ui.PlaceholderAlignment.belowBaseline: {
358 359
          assert(
            RenderObject.debugCheckingIntrinsics,
360
            'Intrinsics are not available for PlaceholderAlignment.baseline, '
361 362
            'PlaceholderAlignment.aboveBaseline, or PlaceholderAlignment.belowBaseline.',
          );
363 364 365 366 367 368 369 370 371 372 373 374 375
          return false;
        }
        case ui.PlaceholderAlignment.top:
        case ui.PlaceholderAlignment.middle:
        case ui.PlaceholderAlignment.bottom: {
          continue;
        }
      }
    }
    return true;
  }

  void _computeChildrenWidthWithMaxIntrinsics(double height) {
376 377
    RenderBox? child = firstChild;
    final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>.filled(childCount, PlaceholderDimensions.empty, growable: false);
378 379 380
    int childIndex = 0;
    while (child != null) {
      // Height and baseline is irrelevant as all text will be laid
381
      // out in a single line. Therefore, using 0.0 as a dummy for the height.
382
      placeholderDimensions[childIndex] = PlaceholderDimensions(
383
        size: Size(child.getMaxIntrinsicWidth(double.infinity), 0.0),
384 385 386 387 388 389
        alignment: _placeholderSpans[childIndex].alignment,
        baseline: _placeholderSpans[childIndex].baseline,
      );
      child = childAfter(child);
      childIndex += 1;
    }
390
    _textPainter.setPlaceholderDimensions(placeholderDimensions);
391 392 393
  }

  void _computeChildrenWidthWithMinIntrinsics(double height) {
394 395
    RenderBox? child = firstChild;
    final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>.filled(childCount, PlaceholderDimensions.empty, growable: false);
396 397
    int childIndex = 0;
    while (child != null) {
398 399
      // Height and baseline is irrelevant; only looking for the widest word or
      // placeholder. Therefore, using 0.0 as a dummy for height.
400
      placeholderDimensions[childIndex] = PlaceholderDimensions(
401
        size: Size(child.getMinIntrinsicWidth(double.infinity), 0.0),
402 403 404 405 406 407
        alignment: _placeholderSpans[childIndex].alignment,
        baseline: _placeholderSpans[childIndex].baseline,
      );
      child = childAfter(child);
      childIndex += 1;
    }
408
    _textPainter.setPlaceholderDimensions(placeholderDimensions);
409 410 411
  }

  void _computeChildrenHeightWithMinIntrinsics(double width) {
412 413
    RenderBox? child = firstChild;
    final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>.filled(childCount, PlaceholderDimensions.empty, growable: false);
414
    int childIndex = 0;
415
    // Takes textScaleFactor into account because the content of the placeholder
416
    // span will be scaled up when it paints.
417
    width = width / textScaleFactor;
418
    while (child != null) {
419
      final Size size = child.getDryLayout(BoxConstraints(maxWidth: width));
420
      placeholderDimensions[childIndex] = PlaceholderDimensions(
421
        size: size,
422 423 424 425 426 427
        alignment: _placeholderSpans[childIndex].alignment,
        baseline: _placeholderSpans[childIndex].baseline,
      );
      child = childAfter(child);
      childIndex += 1;
    }
428
    _textPainter.setPlaceholderDimensions(placeholderDimensions);
429 430
  }

431
  @override
432
  bool hitTestSelf(Offset position) => true;
Adam Barth's avatar
Adam Barth committed
433

434
  @override
435
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
436 437 438 439 440 441 442 443 444 445 446 447
    // Hit test text spans.
    late final bool hitText;
    final TextPosition textPosition = _textPainter.getPositionForOffset(position);
    final InlineSpan? span = _textPainter.text!.getSpanForPosition(textPosition);
    if (span != null && span is HitTestTarget) {
      result.add(HitTestEntry(span as HitTestTarget));
      hitText = true;
    } else {
      hitText = false;
    }

    // Hit test render object children
448
    RenderBox? child = firstChild;
449 450
    int childIndex = 0;
    while (child != null && childIndex < _textPainter.inlinePlaceholderBoxes!.length) {
451
      final TextParentData textParentData = child.parentData! as TextParentData;
452 453 454 455 456 457 458 459 460
      final Matrix4 transform = Matrix4.translationValues(
        textParentData.offset.dx,
        textParentData.offset.dy,
        0.0,
      )..scale(
        textParentData.scale,
        textParentData.scale,
        textParentData.scale,
      );
461 462 463
      final bool isHit = result.addWithPaintTransform(
        transform: transform,
        position: position,
464
        hitTest: (BoxHitTestResult result, Offset? transformed) {
465
          assert(() {
466 467
            final Offset manualPosition = (position - textParentData.offset) / textParentData.scale!;
            return (transformed!.dx - manualPosition.dx).abs() < precisionErrorTolerance
468 469
              && (transformed.dy - manualPosition.dy).abs() < precisionErrorTolerance;
          }());
470
          return child!.hitTest(result, position: transformed!);
471 472 473 474 475 476
        },
      );
      if (isHit) {
        return true;
      }
      child = childAfter(child);
477
      childIndex += 1;
478
    }
479
    return hitText;
480 481
  }

482
  bool _needsClipping = false;
483
  ui.Shader? _overflowShader;
484

485
  /// Whether this paragraph currently has a [dart:ui.Shader] for its overflow
Ian Hickson's avatar
Ian Hickson committed
486
  /// effect.
487 488
  ///
  /// Used to test this object. Not for use in production.
489 490 491
  @visibleForTesting
  bool get debugHasOverflowShader => _overflowShader != null;

492 493
  void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
    final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
494 495 496 497 498 499
    _textPainter.layout(
      minWidth: minWidth,
      maxWidth: widthMatters ?
        maxWidth :
        double.infinity,
    );
500 501
  }

502 503 504 505 506 507
  @override
  void systemFontsDidChange() {
    super.systemFontsDidChange();
    _textPainter.markNeedsLayout();
  }

508 509 510 511 512
  // Placeholder dimensions representing the sizes of child inline widgets.
  //
  // These need to be cached because the text painter's placeholder dimensions
  // will be overwritten during intrinsic width/height calculations and must be
  // restored to the original values before final layout and painting.
513
  List<PlaceholderDimensions>? _placeholderDimensions;
514

515
  void _layoutTextWithConstraints(BoxConstraints constraints) {
516
    _textPainter.setPlaceholderDimensions(_placeholderDimensions);
517 518 519 520 521 522 523
    _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
  }

  // Layout the child inline widgets. We then pass the dimensions of the
  // children to _textPainter so that appropriate placeholders can be inserted
  // into the LibTxt layout. This does not do anything if no inline widgets were
  // specified.
524
  List<PlaceholderDimensions> _layoutChildren(BoxConstraints constraints, {bool dry = false}) {
525
    if (childCount == 0) {
526
      return <PlaceholderDimensions>[];
527
    }
528 529
    RenderBox? child = firstChild;
    final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>.filled(childCount, PlaceholderDimensions.empty, growable: false);
530
    int childIndex = 0;
531 532
    // Only constrain the width to the maximum width of the paragraph.
    // Leave height unconstrained, which will overflow if expanded past.
533 534
    BoxConstraints boxConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
    // The content will be enlarged by textScaleFactor during painting phase.
535
    // We reduce constraints by textScaleFactor, so that the content will fit
536
    // into the box once it is enlarged.
537
    boxConstraints = boxConstraints / textScaleFactor;
538
    while (child != null) {
539
      double? baselineOffset;
540 541 542 543 544 545 546 547 548 549
      final Size childSize;
      if (!dry) {
        child.layout(
          boxConstraints,
          parentUsesSize: true,
        );
        childSize = child.size;
        switch (_placeholderSpans[childIndex].alignment) {
          case ui.PlaceholderAlignment.baseline: {
            baselineOffset = child.getDistanceToBaseline(
550
              _placeholderSpans[childIndex].baseline!,
551 552 553 554 555 556 557
            );
            break;
          }
          default: {
            baselineOffset = null;
            break;
          }
558
        }
559 560 561
      } else {
        assert(_placeholderSpans[childIndex].alignment != ui.PlaceholderAlignment.baseline);
        childSize = child.getDryLayout(boxConstraints);
562
      }
563
      placeholderDimensions[childIndex] = PlaceholderDimensions(
564
        size: childSize,
565 566 567 568 569 570 571
        alignment: _placeholderSpans[childIndex].alignment,
        baseline: _placeholderSpans[childIndex].baseline,
        baselineOffset: baselineOffset,
      );
      child = childAfter(child);
      childIndex += 1;
    }
572
    return placeholderDimensions;
573 574 575 576 577
  }

  // Iterate through the laid-out children and set the parentData offsets based
  // off of the placeholders inserted for each child.
  void _setParentData() {
578
    RenderBox? child = firstChild;
579
    int childIndex = 0;
580
    while (child != null && childIndex < _textPainter.inlinePlaceholderBoxes!.length) {
581
      final TextParentData textParentData = child.parentData! as TextParentData;
582
      textParentData.offset = Offset(
583 584
        _textPainter.inlinePlaceholderBoxes![childIndex].left,
        _textPainter.inlinePlaceholderBoxes![childIndex].top,
585
      );
586
      textParentData.scale = _textPainter.inlinePlaceholderScales![childIndex];
587 588 589 590 591
      child = childAfter(child);
      childIndex += 1;
    }
  }

592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618
  bool _canComputeDryLayout() {
    // Dry layout cannot be calculated without a full layout for
    // alignments that require the baseline (baseline, aboveBaseline,
    // belowBaseline).
    for (final PlaceholderSpan span in _placeholderSpans) {
      switch (span.alignment) {
        case ui.PlaceholderAlignment.baseline:
        case ui.PlaceholderAlignment.aboveBaseline:
        case ui.PlaceholderAlignment.belowBaseline: {
          return false;
        }
        case ui.PlaceholderAlignment.top:
        case ui.PlaceholderAlignment.middle:
        case ui.PlaceholderAlignment.bottom: {
          continue;
        }
      }
    }
    return true;
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    if (!_canComputeDryLayout()) {
      assert(debugCannotComputeDryLayout(
        reason: 'Dry layout not available for alignments that require baseline.',
      ));
619
      return Size.zero;
620 621 622 623 624 625
    }
    _textPainter.setPlaceholderDimensions(_layoutChildren(constraints, dry: true));
    _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
    return constraints.constrain(_textPainter.size);
  }

626
  @override
627
  void performLayout() {
628
    final BoxConstraints constraints = this.constraints;
629
    _placeholderDimensions = _layoutChildren(constraints);
630
    _layoutTextWithConstraints(constraints);
631 632
    _setParentData();

633 634 635 636 637
    // We grab _textPainter.size and _textPainter.didExceedMaxLines here because
    // assigning to `size` will trigger us to validate our intrinsic sizes,
    // which will change _textPainter's layout because the intrinsic size
    // calculations are destructive. Other _textPainter state will also be
    // affected. See also RenderEditable which has a similar issue.
638
    final Size textSize = _textPainter.size;
639
    final bool textDidExceedMaxLines = _textPainter.didExceedMaxLines;
640
    size = constraints.constrain(textSize);
641

642
    final bool didOverflowHeight = size.height < textSize.height || textDidExceedMaxLines;
643
    final bool didOverflowWidth = size.width < textSize.width;
644 645 646 647 648
    // TODO(abarth): We're only measuring the sizes of the line boxes here. If
    // the glyphs draw outside the line boxes, we might think that there isn't
    // visual overflow when there actually is visual overflow. This can become
    // a problem if we start having horizontal overflow and introduce a clip
    // that affects the actual (but undetected) vertical overflow.
649 650
    final bool hasVisualOverflow = didOverflowWidth || didOverflowHeight;
    if (hasVisualOverflow) {
651
      switch (_overflow) {
652 653 654 655
        case TextOverflow.visible:
          _needsClipping = false;
          _overflowShader = null;
          break;
656
        case TextOverflow.clip:
657
        case TextOverflow.ellipsis:
658
          _needsClipping = true;
659 660 661
          _overflowShader = null;
          break;
        case TextOverflow.fade:
Ian Hickson's avatar
Ian Hickson committed
662
          assert(textDirection != null);
663
          _needsClipping = true;
664
          final TextPainter fadeSizePainter = TextPainter(
665
            text: TextSpan(style: _textPainter.text!.style, text: '\u2026'),
Ian Hickson's avatar
Ian Hickson committed
666 667
            textDirection: textDirection,
            textScaleFactor: textScaleFactor,
668
            locale: locale,
669
          )..layout();
670
          if (didOverflowWidth) {
Ian Hickson's avatar
Ian Hickson committed
671 672 673 674 675 676 677 678 679 680 681
            double fadeEnd, fadeStart;
            switch (textDirection) {
              case TextDirection.rtl:
                fadeEnd = 0.0;
                fadeStart = fadeSizePainter.width;
                break;
              case TextDirection.ltr:
                fadeEnd = size.width;
                fadeStart = fadeEnd - fadeSizePainter.width;
                break;
            }
682 683 684
            _overflowShader = ui.Gradient.linear(
              Offset(fadeStart, 0.0),
              Offset(fadeEnd, 0.0),
685
              <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
686 687 688 689
            );
          } else {
            final double fadeEnd = size.height;
            final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0;
690 691 692
            _overflowShader = ui.Gradient.linear(
              Offset(0.0, fadeStart),
              Offset(0.0, fadeEnd),
693
              <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
694 695
            );
          }
696 697 698
          break;
      }
    } else {
699
      _needsClipping = false;
700 701
      _overflowShader = null;
    }
702 703
  }

704
  @override
705
  void paint(PaintingContext context, Offset offset) {
706 707
    // Ideally we could compute the min/max intrinsic width/height with a
    // non-destructive operation. However, currently, computing these values
708 709
    // will destroy state inside the painter. If that happens, we need to get
    // back the correct state by calling _layout again.
710
    //
711 712
    // TODO(abarth): Make computing the min/max intrinsic width/height a
    //  non-destructive operation.
713 714 715
    //
    // If you remove this call, make sure that changing the textAlign still
    // works properly.
716
    _layoutTextWithConstraints(constraints);
717 718 719

    assert(() {
      if (debugRepaintTextRainbowEnabled) {
720
        final Paint paint = Paint()
721
          ..color = debugCurrentRepaintColor.toColor();
722
        context.canvas.drawRect(offset & size, paint);
723 724
      }
      return true;
725
    }());
726

727
    if (_needsClipping) {
728
      final Rect bounds = offset & size;
729
      if (_overflowShader != null) {
730 731
        // This layer limits what the shader below blends with to be just the
        // text (as opposed to the text and its background).
732
        context.canvas.saveLayer(bounds, Paint());
733
      } else {
734
        context.canvas.save();
735
      }
736 737 738 739
      context.canvas.clipRect(bounds);
    }
    _textPainter.paint(context.canvas, offset);

740
    RenderBox? child = firstChild;
741
    int childIndex = 0;
742 743 744 745
    // childIndex might be out of index of placeholder boxes. This can happen
    // if engine truncates children due to ellipsis. Sadly, we would not know
    // it until we finish layout, and RenderObject is in immutable state at
    // this point.
746
    while (child != null && childIndex < _textPainter.inlinePlaceholderBoxes!.length) {
747
      final TextParentData textParentData = child.parentData! as TextParentData;
748

749
      final double scale = textParentData.scale!;
750 751 752 753 754 755
      context.pushTransform(
        needsCompositing,
        offset + textParentData.offset,
        Matrix4.diagonal3Values(scale, scale, scale),
        (PaintingContext context, Offset offset) {
          context.paintChild(
756
            child!,
757 758 759 760 761 762
            offset,
          );
        },
      );
      child = childAfter(child);
      childIndex += 1;
763
    }
764
    if (_needsClipping) {
765
      if (_overflowShader != null) {
766
        context.canvas.translate(offset.dx, offset.dy);
767
        final Paint paint = Paint()
768
          ..blendMode = BlendMode.modulate
769
          ..shader = _overflowShader;
770
        context.canvas.drawRect(Offset.zero & size, paint);
771
      }
772
      context.canvas.restore();
773
    }
774 775
  }

776 777 778 779
  /// Returns the offset at which to paint the caret.
  ///
  /// Valid only after [layout].
  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
780
    assert(!debugNeedsLayout);
781 782 783 784
    _layoutTextWithConstraints(constraints);
    return _textPainter.getOffsetForCaret(position, caretPrototype);
  }

785 786 787 788 789 790 791 792 793
  /// {@macro flutter.painting.textPainter.getFullHeightForCaret}
  ///
  /// Valid only after [layout].
  double? getFullHeightForCaret(TextPosition position) {
    assert(!debugNeedsLayout);
    _layoutTextWithConstraints(constraints);
    return _textPainter.getFullHeightForCaret(position, Rect.zero);
  }

794 795 796 797 798 799 800 801
  /// Returns a list of rects that bound the given selection.
  ///
  /// A given selection might have more than one rect if this text painter
  /// contains bidirectional text because logically contiguous text might not be
  /// visually contiguous.
  ///
  /// Valid only after [layout].
  List<ui.TextBox> getBoxesForSelection(TextSelection selection) {
802
    assert(!debugNeedsLayout);
803 804 805 806 807 808 809 810
    _layoutTextWithConstraints(constraints);
    return _textPainter.getBoxesForSelection(selection);
  }

  /// Returns the position within the text for the given pixel offset.
  ///
  /// Valid only after [layout].
  TextPosition getPositionForOffset(Offset offset) {
811
    assert(!debugNeedsLayout);
812 813 814 815 816 817 818 819 820 821 822 823 824 825
    _layoutTextWithConstraints(constraints);
    return _textPainter.getPositionForOffset(offset);
  }

  /// Returns the text range of the word at the given offset. Characters not
  /// part of a word, such as spaces, symbols, and punctuation, have word breaks
  /// on both sides. In such cases, this method will return a text range that
  /// contains the given text position.
  ///
  /// Word boundaries are defined more precisely in Unicode Standard Annex #29
  /// <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
  ///
  /// Valid only after [layout].
  TextRange getWordBoundary(TextPosition position) {
826
    assert(!debugNeedsLayout);
827 828 829 830
    _layoutTextWithConstraints(constraints);
    return _textPainter.getWordBoundary(position);
  }

831 832 833 834 835 836 837 838 839 840 841 842 843 844
  /// Returns the size of the text as laid out.
  ///
  /// This can differ from [size] if the text overflowed or if the [constraints]
  /// provided by the parent [RenderObject] forced the layout to be bigger than
  /// necessary for the given [text].
  ///
  /// This returns the [TextPainter.size] of the underlying [TextPainter].
  ///
  /// Valid only after [layout].
  Size get textSize {
    assert(!debugNeedsLayout);
    return _textPainter.size;
  }

845 846
  /// Collected during [describeSemanticsConfiguration], used by
  /// [assembleSemanticsNode] and [_combineSemanticsInfo].
847
  List<InlineSpanSemanticsInformation>? _semanticsInfo;
848

849
  @override
850 851
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
852 853
    _semanticsInfo = text.getSemanticsInformation();

854
    if (_semanticsInfo!.any((InlineSpanSemanticsInformation info) => info.recognizer != null)) {
855 856 857
      config.explicitChildNodes = true;
      config.isSemanticBoundary = true;
    } else {
858
      final StringBuffer buffer = StringBuffer();
859
      for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
860 861 862
        buffer.write(info.semanticsLabel ?? info.text);
      }
      config.label = buffer.toString();
863 864 865 866
      config.textDirection = textDirection;
    }
  }

867 868 869 870
  // Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
  // can be re-used when [assembleSemanticsNode] is called again. This ensures
  // stable ids for the [SemanticsNode]s of [TextSpan]s across
  // [assembleSemanticsNode] invocations.
871
  Queue<SemanticsNode>? _cachedChildNodes;
872

873 874
  @override
  void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
875
    assert(_semanticsInfo != null && _semanticsInfo!.isNotEmpty);
876 877 878
    final List<SemanticsNode> newChildren = <SemanticsNode>[];
    TextDirection currentDirection = textDirection;
    Rect currentRect;
879 880 881
    double ordinal = 0.0;
    int start = 0;
    int placeholderIndex = 0;
882
    int childIndex = 0;
883
    RenderBox? child = firstChild;
884
    final Queue<SemanticsNode> newChildCache = Queue<SemanticsNode>();
885
    for (final InlineSpanSemanticsInformation info in combineSemanticsInfo(_semanticsInfo!)) {
886 887 888 889
      final TextSelection selection = TextSelection(
        baseOffset: start,
        extentOffset: start + info.text.length,
      );
890
      start += info.text.length;
891

892
      if (info.isPlaceholder) {
893
        // A placeholder span may have 0 to multiple semantics nodes, we need
894 895 896 897
        // to annotate all of the semantics nodes belong to this span.
        while (children.length > childIndex &&
               children.elementAt(childIndex).isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
          final SemanticsNode childNode = children.elementAt(childIndex);
898
          final TextParentData parentData = child!.parentData! as TextParentData;
899 900 901 902 903 904 905
          childNode.rect = Rect.fromLTWH(
            childNode.rect.left,
            childNode.rect.top,
            childNode.rect.width * parentData.scale!,
            childNode.rect.height * parentData.scale!,
          );
          newChildren.add(childNode);
906
          childIndex += 1;
907
        }
908 909
        child = childAfter(child!);
        placeholderIndex += 1;
910
      } else {
911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937
        final TextDirection initialDirection = currentDirection;
        final List<ui.TextBox> rects = getBoxesForSelection(selection);
        if (rects.isEmpty) {
          continue;
        }
        Rect rect = rects.first.toRect();
        currentDirection = rects.first.direction;
        for (final ui.TextBox textBox in rects.skip(1)) {
          rect = rect.expandToInclude(textBox.toRect());
          currentDirection = textBox.direction;
        }
        // Any of the text boxes may have had infinite dimensions.
        // We shouldn't pass infinite dimensions up to the bridges.
        rect = Rect.fromLTWH(
          math.max(0.0, rect.left),
          math.max(0.0, rect.top),
          math.min(rect.width, constraints.maxWidth),
          math.min(rect.height, constraints.maxHeight),
        );
        // round the current rectangle to make this API testable and add some
        // padding so that the accessibility rects do not overlap with the text.
        currentRect = Rect.fromLTRB(
          rect.left.floorToDouble() - 4.0,
          rect.top.floorToDouble() - 4.0,
          rect.right.ceilToDouble() + 4.0,
          rect.bottom.ceilToDouble() + 4.0,
        );
938 939 940 941
        final SemanticsConfiguration configuration = SemanticsConfiguration()
          ..sortKey = OrdinalSortKey(ordinal++)
          ..textDirection = initialDirection
          ..label = info.semanticsLabel ?? info.text;
942
        final GestureRecognizer? recognizer = info.recognizer;
943 944
        if (recognizer != null) {
          if (recognizer is TapGestureRecognizer) {
945 946 947 948
            if (recognizer.onTap != null) {
              configuration.onTap = recognizer.onTap;
              configuration.isLink = true;
            }
949
          } else if (recognizer is DoubleTapGestureRecognizer) {
950 951 952 953
            if (recognizer.onDoubleTap != null) {
              configuration.onTap = recognizer.onDoubleTap;
              configuration.isLink = true;
            }
954
          } else if (recognizer is LongPressGestureRecognizer) {
955 956 957
            if (recognizer.onLongPress != null) {
              configuration.onLongPress = recognizer.onLongPress;
            }
958
          } else {
959
            assert(false, '${recognizer.runtimeType} is not supported.');
960 961
          }
        }
962
        final SemanticsNode newChild = (_cachedChildNodes?.isNotEmpty == true)
963
            ? _cachedChildNodes!.removeFirst()
964 965 966 967 968 969
            : SemanticsNode();
        newChild
          ..updateWith(config: configuration)
          ..rect = currentRect;
        newChildCache.addLast(newChild);
        newChildren.add(newChild);
970 971
      }
    }
972 973 974 975
    // Makes sure we annotated all of the semantics children.
    assert(childIndex == children.length);
    assert(child == null);

976
    _cachedChildNodes = newChildCache;
977
    node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
Hixie's avatar
Hixie committed
978
  }
979

980 981 982 983 984 985
  @override
  void clearSemantics() {
    super.clearSemantics();
    _cachedChildNodes = null;
  }

986
  @override
987
  List<DiagnosticsNode> debugDescribeChildren() {
988 989 990 991
    return <DiagnosticsNode>[
      text.toDiagnosticsNode(
        name: 'text',
        style: DiagnosticsTreeStyle.transition,
992
      ),
993
    ];
994
  }
Ian Hickson's avatar
Ian Hickson committed
995 996

  @override
997 998
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
999 1000
    properties.add(EnumProperty<TextAlign>('textAlign', textAlign));
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
1001 1002 1003 1004 1005 1006 1007
    properties.add(
      FlagProperty(
        'softWrap',
        value: softWrap,
        ifTrue: 'wrapping at box width',
        ifFalse: 'no wrapping except at line break characters',
        showName: true,
1008
      ),
1009
    );
1010
    properties.add(EnumProperty<TextOverflow>('overflow', overflow));
1011 1012 1013 1014 1015
    properties.add(
      DoubleProperty(
        'textScaleFactor',
        textScaleFactor,
        defaultValue: 1.0,
1016
      ),
1017 1018 1019 1020 1021 1022
    );
    properties.add(
      DiagnosticsProperty<Locale>(
        'locale',
        locale,
        defaultValue: null,
1023
      ),
1024
    );
1025
    properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
Ian Hickson's avatar
Ian Hickson committed
1026
  }
1027
}