paragraph.dart 38.5 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, BoxHeightStyle, BoxWidthStyle;
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
/// Parent data for use with [RenderParagraph] and [RenderEditable].
22 23
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
  AttributedString? _cachedAttributedLabel;
  List<InlineSpanSemanticsInformation>? _cachedCombinedSemanticsInfos;
123

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

151
  late List<PlaceholderSpan> _placeholderSpans;
152 153 154 155
  void _extractPlaceholderSpans(InlineSpan span) {
    _placeholderSpans = <PlaceholderSpan>[];
    span.visitChildren((InlineSpan span) {
      if (span is PlaceholderSpan) {
156
        _placeholderSpans.add(span);
157 158 159 160 161
      }
      return true;
    });
  }

162 163
  /// How the text should be aligned horizontally.
  TextAlign get textAlign => _textPainter.textAlign;
164
  set textAlign(TextAlign value) {
Ian Hickson's avatar
Ian Hickson committed
165
    assert(value != null);
166 167 168 169 170 171
    if (_textPainter.textAlign == value)
      return;
    _textPainter.textAlign = value;
    markNeedsPaint();
  }

Ian Hickson's avatar
Ian Hickson committed
172 173 174 175 176 177 178 179 180
  /// 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]
181
  /// context, the English phrase will be on the right and the Hebrew phrase on
Ian Hickson's avatar
Ian Hickson committed
182 183 184
  /// its left.
  ///
  /// This must not be null.
185
  TextDirection get textDirection => _textPainter.textDirection!;
Ian Hickson's avatar
Ian Hickson committed
186 187 188 189 190 191 192 193
  set textDirection(TextDirection value) {
    assert(value != null);
    if (_textPainter.textDirection == value)
      return;
    _textPainter.textDirection = value;
    markNeedsLayout();
  }

194 195
  /// Whether the text should break at soft line breaks.
  ///
196 197 198 199 200
  /// 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.
201 202
  bool get softWrap => _softWrap;
  bool _softWrap;
203
  set softWrap(bool value) {
204 205 206 207 208 209 210 211 212 213
    assert(value != null);
    if (_softWrap == value)
      return;
    _softWrap = value;
    markNeedsLayout();
  }

  /// How visual overflow should be handled.
  TextOverflow get overflow => _overflow;
  TextOverflow _overflow;
214
  set overflow(TextOverflow value) {
215 216 217 218
    assert(value != null);
    if (_overflow == value)
      return;
    _overflow = value;
219
    _textPainter.ellipsis = value == TextOverflow.ellipsis ? _kEllipsis : null;
220
    markNeedsLayout();
221 222
  }

223 224 225 226 227 228 229 230 231 232 233 234 235 236
  /// 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();
  }

237 238 239
  /// 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].
240
  int? get maxLines => _textPainter.maxLines;
241 242
  /// The value may be null. If it is not null, then it must be greater than
  /// zero.
243
  set maxLines(int? value) {
244
    assert(value == null || value > 0);
245 246 247 248 249 250 251
    if (_textPainter.maxLines == value)
      return;
    _textPainter.maxLines = value;
    _overflowShader = null;
    markNeedsLayout();
  }

252 253
  /// Used by this paragraph's internal [TextPainter] to select a
  /// locale-specific font.
254
  ///
255 256 257 258
  /// 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.
259
  Locale? get locale => _textPainter.locale;
260
  /// The value may be null.
261
  set locale(Locale? value) {
262 263
    if (_textPainter.locale == value)
      return;
264
    _textPainter.locale = value;
265 266 267 268
    _overflowShader = null;
    markNeedsLayout();
  }

269
  /// {@macro flutter.painting.textPainter.strutStyle}
270
  StrutStyle? get strutStyle => _textPainter.strutStyle;
271
  /// The value may be null.
272
  set strutStyle(StrutStyle? value) {
273 274 275 276 277 278 279
    if (_textPainter.strutStyle == value)
      return;
    _textPainter.strutStyle = value;
    _overflowShader = null;
    markNeedsLayout();
  }

280
  /// {@macro flutter.painting.textPainter.textWidthBasis}
281 282 283 284 285 286 287 288 289 290
  TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis;
  set textWidthBasis(TextWidthBasis value) {
    assert(value != null);
    if (_textPainter.textWidthBasis == value)
      return;
    _textPainter.textWidthBasis = value;
    _overflowShader = null;
    markNeedsLayout();
  }

291
  /// {@macro dart.ui.textHeightBehavior}
292 293
  ui.TextHeightBehavior? get textHeightBehavior => _textPainter.textHeightBehavior;
  set textHeightBehavior(ui.TextHeightBehavior? value) {
294 295 296 297 298 299 300
    if (_textPainter.textHeightBehavior == value)
      return;
    _textPainter.textHeightBehavior = value;
    _overflowShader = null;
    markNeedsLayout();
  }

301
  @override
302
  double computeMinIntrinsicWidth(double height) {
303 304 305 306 307
    if (!_canComputeIntrinsics()) {
      return 0.0;
    }
    _computeChildrenWidthWithMinIntrinsics(height);
    _layoutText(); // layout with infinite width.
308
    return _textPainter.minIntrinsicWidth;
309 310
  }

311
  @override
312
  double computeMaxIntrinsicWidth(double height) {
313 314 315 316 317
    if (!_canComputeIntrinsics()) {
      return 0.0;
    }
    _computeChildrenWidthWithMaxIntrinsics(height);
    _layoutText(); // layout with infinite width.
318
    return _textPainter.maxIntrinsicWidth;
319 320
  }

321
  double _computeIntrinsicHeight(double width) {
322 323 324 325
    if (!_canComputeIntrinsics()) {
      return 0.0;
    }
    _computeChildrenHeightWithMinIntrinsics(width);
326 327
    _layoutText(minWidth: width, maxWidth: width);
    return _textPainter.height;
328 329
  }

330
  @override
331 332
  double computeMinIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
333 334
  }

335
  @override
336 337
  double computeMaxIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
338 339
  }

340
  @override
341
  double computeDistanceToActualBaseline(TextBaseline baseline) {
342
    assert(!debugNeedsLayout);
343 344
    assert(constraints != null);
    assert(constraints.debugAssertIsValid());
345
    _layoutTextWithConstraints(constraints);
346 347 348 349 350 351
    // 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).
352
    return _textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic);
353 354
  }

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

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

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

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

437
  @override
438
  bool hitTestSelf(Offset position) => true;
Adam Barth's avatar
Adam Barth committed
439

440
  @override
441
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
442
    // Hit test text spans.
443
    bool hitText = false;
444 445 446 447 448 449 450 451
    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;
    }

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

486
  bool _needsClipping = false;
487
  ui.Shader? _overflowShader;
488

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

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

506 507 508 509 510 511
  @override
  void systemFontsDidChange() {
    super.systemFontsDidChange();
    _textPainter.markNeedsLayout();
  }

512 513 514 515 516
  // 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.
517
  List<PlaceholderDimensions>? _placeholderDimensions;
518

519
  void _layoutTextWithConstraints(BoxConstraints constraints) {
520
    _textPainter.setPlaceholderDimensions(_placeholderDimensions);
521 522 523 524 525 526 527
    _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.
528
  List<PlaceholderDimensions> _layoutChildren(BoxConstraints constraints, {bool dry = false}) {
529
    if (childCount == 0) {
530
      return <PlaceholderDimensions>[];
531
    }
532
    RenderBox? child = firstChild;
533
    final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>.filled(childCount, PlaceholderDimensions.empty);
534
    int childIndex = 0;
535 536
    // Only constrain the width to the maximum width of the paragraph.
    // Leave height unconstrained, which will overflow if expanded past.
537 538
    BoxConstraints boxConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
    // The content will be enlarged by textScaleFactor during painting phase.
539
    // We reduce constraints by textScaleFactor, so that the content will fit
540
    // into the box once it is enlarged.
541
    boxConstraints = boxConstraints / textScaleFactor;
542
    while (child != null) {
543
      double? baselineOffset;
544 545 546 547 548 549 550 551
      final Size childSize;
      if (!dry) {
        child.layout(
          boxConstraints,
          parentUsesSize: true,
        );
        childSize = child.size;
        switch (_placeholderSpans[childIndex].alignment) {
552
          case ui.PlaceholderAlignment.baseline:
553
            baselineOffset = child.getDistanceToBaseline(
554
              _placeholderSpans[childIndex].baseline!,
555 556
            );
            break;
557 558 559 560 561
          case ui.PlaceholderAlignment.aboveBaseline:
          case ui.PlaceholderAlignment.belowBaseline:
          case ui.PlaceholderAlignment.bottom:
          case ui.PlaceholderAlignment.middle:
          case ui.PlaceholderAlignment.top:
562 563
            baselineOffset = null;
            break;
564
        }
565 566 567
      } else {
        assert(_placeholderSpans[childIndex].alignment != ui.PlaceholderAlignment.baseline);
        childSize = child.getDryLayout(boxConstraints);
568
      }
569
      placeholderDimensions[childIndex] = PlaceholderDimensions(
570
        size: childSize,
571 572 573 574 575 576 577
        alignment: _placeholderSpans[childIndex].alignment,
        baseline: _placeholderSpans[childIndex].baseline,
        baselineOffset: baselineOffset,
      );
      child = childAfter(child);
      childIndex += 1;
    }
578
    return placeholderDimensions;
579 580 581 582 583
  }

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

598 599 600 601 602 603 604 605
  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:
606
        case ui.PlaceholderAlignment.belowBaseline:
607 608 609
          return false;
        case ui.PlaceholderAlignment.top:
        case ui.PlaceholderAlignment.middle:
610
        case ui.PlaceholderAlignment.bottom:
611 612 613 614 615 616 617 618 619 620 621 622
          continue;
      }
    }
    return true;
  }

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

630
  @override
631
  void performLayout() {
632
    final BoxConstraints constraints = this.constraints;
633
    _placeholderDimensions = _layoutChildren(constraints);
634
    _layoutTextWithConstraints(constraints);
635 636
    _setParentData();

637 638 639 640 641
    // 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.
642
    final Size textSize = _textPainter.size;
643
    final bool textDidExceedMaxLines = _textPainter.didExceedMaxLines;
644
    size = constraints.constrain(textSize);
645

646
    final bool didOverflowHeight = size.height < textSize.height || textDidExceedMaxLines;
647
    final bool didOverflowWidth = size.width < textSize.width;
648 649 650 651 652
    // 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.
653 654
    final bool hasVisualOverflow = didOverflowWidth || didOverflowHeight;
    if (hasVisualOverflow) {
655
      switch (_overflow) {
656 657 658 659
        case TextOverflow.visible:
          _needsClipping = false;
          _overflowShader = null;
          break;
660
        case TextOverflow.clip:
661
        case TextOverflow.ellipsis:
662
          _needsClipping = true;
663 664 665
          _overflowShader = null;
          break;
        case TextOverflow.fade:
Ian Hickson's avatar
Ian Hickson committed
666
          assert(textDirection != null);
667
          _needsClipping = true;
668
          final TextPainter fadeSizePainter = TextPainter(
669
            text: TextSpan(style: _textPainter.text!.style, text: '\u2026'),
Ian Hickson's avatar
Ian Hickson committed
670 671
            textDirection: textDirection,
            textScaleFactor: textScaleFactor,
672
            locale: locale,
673
          )..layout();
674
          if (didOverflowWidth) {
Ian Hickson's avatar
Ian Hickson committed
675 676 677 678 679 680 681 682 683 684 685
            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;
            }
686 687 688
            _overflowShader = ui.Gradient.linear(
              Offset(fadeStart, 0.0),
              Offset(fadeEnd, 0.0),
689
              <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
690 691 692 693
            );
          } else {
            final double fadeEnd = size.height;
            final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0;
694 695 696
            _overflowShader = ui.Gradient.linear(
              Offset(0.0, fadeStart),
              Offset(0.0, fadeEnd),
697
              <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
698 699
            );
          }
700 701 702
          break;
      }
    } else {
703
      _needsClipping = false;
704 705
      _overflowShader = null;
    }
706 707
  }

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

    assert(() {
      if (debugRepaintTextRainbowEnabled) {
724
        final Paint paint = Paint()
725
          ..color = debugCurrentRepaintColor.toColor();
726
        context.canvas.drawRect(offset & size, paint);
727 728
      }
      return true;
729
    }());
730

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

744
    RenderBox? child = firstChild;
745
    int childIndex = 0;
746 747 748 749
    // 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.
750
    while (child != null && childIndex < _textPainter.inlinePlaceholderBoxes!.length) {
751
      final TextParentData textParentData = child.parentData! as TextParentData;
752

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

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

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

798 799
  /// Returns a list of rects that bound the given selection.
  ///
800 801 802 803 804 805 806 807
  /// The [boxHeightStyle] and [boxWidthStyle] arguments may be used to select
  /// the shape of the [TextBox]es. These properties default to
  /// [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and
  /// must not be null.
  ///
  /// A given selection might have more than one rect if the [RenderParagraph]
  /// contains multiple [InlineSpan]s or bidirectional text, because logically
  /// contiguous text might not be visually contiguous.
808 809
  ///
  /// Valid only after [layout].
810 811 812 813 814 815 816 817 818 819
  ///
  /// See also:
  ///
  ///  * [TextPainter.getBoxesForSelection], the method in TextPainter to get
  ///    the equivalent boxes.
  List<ui.TextBox> getBoxesForSelection(
    TextSelection selection, {
    ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
    ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
  }) {
820
    assert(!debugNeedsLayout);
821 822
    assert(boxHeightStyle != null);
    assert(boxWidthStyle != null);
823
    _layoutTextWithConstraints(constraints);
824 825 826 827 828
    return _textPainter.getBoxesForSelection(
      selection,
      boxHeightStyle: boxHeightStyle,
      boxWidthStyle: boxWidthStyle,
    );
829 830 831 832 833 834
  }

  /// Returns the position within the text for the given pixel offset.
  ///
  /// Valid only after [layout].
  TextPosition getPositionForOffset(Offset offset) {
835
    assert(!debugNeedsLayout);
836 837 838 839 840 841 842 843 844 845 846 847 848 849
    _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) {
850
    assert(!debugNeedsLayout);
851 852 853 854
    _layoutTextWithConstraints(constraints);
    return _textPainter.getWordBoundary(position);
  }

855 856 857 858 859 860 861 862 863 864 865 866 867 868
  /// 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;
  }

869 870
  /// Collected during [describeSemanticsConfiguration], used by
  /// [assembleSemanticsNode] and [_combineSemanticsInfo].
871
  List<InlineSpanSemanticsInformation>? _semanticsInfo;
872

873
  @override
874 875
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
876 877
    _semanticsInfo = text.getSemanticsInformation();

878
    if (_semanticsInfo!.any((InlineSpanSemanticsInformation info) => info.recognizer != null)) {
879 880 881
      config.explicitChildNodes = true;
      config.isSemanticBoundary = true;
    } else {
882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900
      if (_cachedAttributedLabel == null) {
        final StringBuffer buffer = StringBuffer();
        int offset = 0;
        final List<StringAttribute> attributes = <StringAttribute>[];
        for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
          final String label = info.semanticsLabel ?? info.text;
          for (final StringAttribute infoAttribute in info.stringAttributes) {
            final TextRange originalRange = infoAttribute.range;
            attributes.add(
              infoAttribute.copy(
                  range: TextRange(start: offset + originalRange.start,
                      end: offset + originalRange.end)
              ),
            );
          }
          buffer.write(label);
          offset += label.length;
        }
        _cachedAttributedLabel = AttributedString(buffer.toString(), attributes: attributes);
901
      }
902
      config.attributedLabel = _cachedAttributedLabel!;
903 904 905 906
      config.textDirection = textDirection;
    }
  }

907 908 909 910
  // 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.
911
  Queue<SemanticsNode>? _cachedChildNodes;
912

913 914
  @override
  void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
915
    assert(_semanticsInfo != null && _semanticsInfo!.isNotEmpty);
916 917 918
    final List<SemanticsNode> newChildren = <SemanticsNode>[];
    TextDirection currentDirection = textDirection;
    Rect currentRect;
919 920 921
    double ordinal = 0.0;
    int start = 0;
    int placeholderIndex = 0;
922
    int childIndex = 0;
923
    RenderBox? child = firstChild;
924
    final Queue<SemanticsNode> newChildCache = Queue<SemanticsNode>();
925 926
    _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
    for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) {
927 928 929 930
      final TextSelection selection = TextSelection(
        baseOffset: start,
        extentOffset: start + info.text.length,
      );
931
      start += info.text.length;
932

933
      if (info.isPlaceholder) {
934
        // A placeholder span may have 0 to multiple semantics nodes, we need
935 936 937 938
        // 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);
939
          final TextParentData parentData = child!.parentData! as TextParentData;
940 941 942 943 944 945 946 947 948 949 950
          assert(parentData.scale != null || parentData.offset == Offset.zero);
          // parentData.scale may be null if the render object is truncated.
          if (parentData.scale != null) {
            childNode.rect = Rect.fromLTWH(
              childNode.rect.left,
              childNode.rect.top,
              childNode.rect.width * parentData.scale!,
              childNode.rect.height * parentData.scale!,
            );
            newChildren.add(childNode);
          }
951
          childIndex += 1;
952
        }
953 954
        child = childAfter(child!);
        placeholderIndex += 1;
955
      } else {
956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982
        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,
        );
983 984 985
        final SemanticsConfiguration configuration = SemanticsConfiguration()
          ..sortKey = OrdinalSortKey(ordinal++)
          ..textDirection = initialDirection
986
          ..attributedLabel = AttributedString(info.semanticsLabel ?? info.text, attributes: info.stringAttributes);
987
        final GestureRecognizer? recognizer = info.recognizer;
988 989
        if (recognizer != null) {
          if (recognizer is TapGestureRecognizer) {
990 991 992 993
            if (recognizer.onTap != null) {
              configuration.onTap = recognizer.onTap;
              configuration.isLink = true;
            }
994
          } else if (recognizer is DoubleTapGestureRecognizer) {
995 996 997 998
            if (recognizer.onDoubleTap != null) {
              configuration.onTap = recognizer.onDoubleTap;
              configuration.isLink = true;
            }
999
          } else if (recognizer is LongPressGestureRecognizer) {
1000 1001 1002
            if (recognizer.onLongPress != null) {
              configuration.onLongPress = recognizer.onLongPress;
            }
1003
          } else {
1004
            assert(false, '${recognizer.runtimeType} is not supported.');
1005 1006
          }
        }
1007
        final SemanticsNode newChild = (_cachedChildNodes?.isNotEmpty == true)
1008
            ? _cachedChildNodes!.removeFirst()
1009 1010 1011 1012 1013 1014
            : SemanticsNode();
        newChild
          ..updateWith(config: configuration)
          ..rect = currentRect;
        newChildCache.addLast(newChild);
        newChildren.add(newChild);
1015 1016
      }
    }
1017 1018 1019 1020
    // Makes sure we annotated all of the semantics children.
    assert(childIndex == children.length);
    assert(child == null);

1021
    _cachedChildNodes = newChildCache;
1022
    node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
Hixie's avatar
Hixie committed
1023
  }
1024

1025 1026 1027 1028 1029 1030
  @override
  void clearSemantics() {
    super.clearSemantics();
    _cachedChildNodes = null;
  }

1031
  @override
1032
  List<DiagnosticsNode> debugDescribeChildren() {
1033 1034 1035 1036
    return <DiagnosticsNode>[
      text.toDiagnosticsNode(
        name: 'text',
        style: DiagnosticsTreeStyle.transition,
1037
      ),
1038
    ];
1039
  }
Ian Hickson's avatar
Ian Hickson committed
1040 1041

  @override
1042 1043
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
1044 1045
    properties.add(EnumProperty<TextAlign>('textAlign', textAlign));
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
1046 1047 1048 1049 1050 1051 1052
    properties.add(
      FlagProperty(
        'softWrap',
        value: softWrap,
        ifTrue: 'wrapping at box width',
        ifFalse: 'no wrapping except at line break characters',
        showName: true,
1053
      ),
1054
    );
1055
    properties.add(EnumProperty<TextOverflow>('overflow', overflow));
1056 1057 1058 1059 1060
    properties.add(
      DoubleProperty(
        'textScaleFactor',
        textScaleFactor,
        defaultValue: 1.0,
1061
      ),
1062 1063 1064 1065 1066 1067
    );
    properties.add(
      DiagnosticsProperty<Locale>(
        'locale',
        locale,
        defaultValue: null,
1068
      ),
1069
    );
1070
    properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
Ian Hickson's avatar
Ian Hickson committed
1071
  }
1072
}