paragraph.dart 34.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 6
// @dart = 2.8

7
import 'dart:collection';
8
import 'dart:math' as math;
9
import 'dart:ui' as ui show Gradient, Shader, TextBox, PlaceholderAlignment, TextHeightBehavior;
10

11
import 'package:flutter/foundation.dart';
12
import 'package:flutter/gestures.dart';
13
import 'package:flutter/painting.dart';
14
import 'package:flutter/semantics.dart';
15
import 'package:flutter/services.dart';
16

17
import 'package:vector_math/vector_math_64.dart';
18

19
import 'box.dart';
20
import 'debug.dart';
21
import 'object.dart';
22

23
/// How overflowing text should be handled.
24 25 26
///
/// A [TextOverflow] can be passed to [Text] and [RichText] via their
/// [Text.overflow] and [RichText.overflow] properties respectively.
27 28 29 30 31 32 33 34 35
enum TextOverflow {
  /// Clip the overflowing text to fix its container.
  clip,

  /// Fade the overflowing text to transparent.
  fade,

  /// Use an ellipsis to indicate that the text has overflowed.
  ellipsis,
36 37 38

  /// Render overflowing text outside of its container.
  visible,
39 40
}

41 42
const String _kEllipsis = '\u2026';

43 44 45 46 47 48 49
/// Parent data for use with [RenderParagraph].
class TextParentData extends ContainerBoxParentData<RenderBox> {
  /// The scaling of the text.
  double scale;

  @override
  String toString() {
50 51 52 53 54
    final List<String> values = <String>[
      if (offset != null) 'offset=$offset',
      if (scale != null) 'scale=$scale',
      super.toString(),
    ];
55 56 57 58 59 60 61
    return values.join('; ');
  }
}

/// A render object that displays a paragraph of text.
class RenderParagraph extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, TextParentData>,
62 63
             RenderBoxContainerDefaultsMixin<RenderBox, TextParentData>,
                  RelayoutWhenSystemFontsChangeMixin {
64 65
  /// Creates a paragraph render object.
  ///
Ian Hickson's avatar
Ian Hickson committed
66 67
  /// The [text], [textAlign], [textDirection], [overflow], [softWrap], and
  /// [textScaleFactor] arguments must not be null.
68 69 70
  ///
  /// The [maxLines] property may be null (and indeed defaults to null), but if
  /// it is not null, it must be greater than zero.
71
  RenderParagraph(InlineSpan text, {
72
    TextAlign textAlign = TextAlign.start,
Ian Hickson's avatar
Ian Hickson committed
73
    @required TextDirection textDirection,
74 75 76
    bool softWrap = true,
    TextOverflow overflow = TextOverflow.clip,
    double textScaleFactor = 1.0,
77
    int maxLines,
78
    Locale locale,
79
    StrutStyle strutStyle,
80 81
    TextWidthBasis textWidthBasis = TextWidthBasis.parent,
    ui.TextHeightBehavior textHeightBehavior,
82 83 84 85 86 87 88
    @Deprecated(
      'This parameter is a temporary flag to migrate the internal tests and '
      'should not be used in other contexts. For more details, please check '
      'https://github.com/flutter/flutter/issues/59316. '
      'This feature was deprecated after v1.19.0.'
    )
    bool applyTextScaleFactorToWidgetSpan = false,
89
    List<RenderBox> children,
90 91
  }) : assert(text != null),
       assert(text.debugAssertIsValid()),
Ian Hickson's avatar
Ian Hickson committed
92 93
       assert(textAlign != null),
       assert(textDirection != null),
94 95 96
       assert(softWrap != null),
       assert(overflow != null),
       assert(textScaleFactor != null),
97
       assert(maxLines == null || maxLines > 0),
98
       assert(textWidthBasis != null),
99
       _softWrap = softWrap,
100
       _overflow = overflow,
101
       _applyTextScaleFactorToWidgetSpan = applyTextScaleFactorToWidgetSpan,
102
       _textPainter = TextPainter(
103 104
         text: text,
         textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
105
         textDirection: textDirection,
106 107 108
         textScaleFactor: textScaleFactor,
         maxLines: maxLines,
         ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
109
         locale: locale,
110
         strutStyle: strutStyle,
111
         textWidthBasis: textWidthBasis,
112
         textHeightBehavior: textHeightBehavior
113 114 115 116 117 118 119 120 121 122
       ) {
    addAll(children);
    _extractPlaceholderSpans(text);
  }

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

124
  final TextPainter _textPainter;
125

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

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

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

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

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

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

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

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

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

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

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

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

301 302
  final bool _applyTextScaleFactorToWidgetSpan;

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

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

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

332
  @override
333 334
  double computeMinIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
335 336
  }

337
  @override
338 339
  double computeMaxIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
340 341
  }

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

357 358 359 360
  // Intrinsics cannot be calculated without a full layout for
  // alignments that require the baseline (baseline, aboveBaseline,
  // belowBaseline).
  bool _canComputeIntrinsics() {
361
    for (final PlaceholderSpan span in _placeholderSpans) {
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
      switch (span.alignment) {
        case ui.PlaceholderAlignment.baseline:
        case ui.PlaceholderAlignment.aboveBaseline:
        case ui.PlaceholderAlignment.belowBaseline: {
          assert(RenderObject.debugCheckingIntrinsics,
            'Intrinsics are not available for PlaceholderAlignment.baseline, '
            'PlaceholderAlignment.aboveBaseline, or PlaceholderAlignment.belowBaseline,');
          return false;
        }
        case ui.PlaceholderAlignment.top:
        case ui.PlaceholderAlignment.middle:
        case ui.PlaceholderAlignment.bottom: {
          continue;
        }
      }
    }
    return true;
  }

  void _computeChildrenWidthWithMaxIntrinsics(double height) {
    RenderBox child = firstChild;
    final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>(childCount);
    int childIndex = 0;
    while (child != null) {
      // Height and baseline is irrelevant as all text will be laid
      // out in a single line.
      placeholderDimensions[childIndex] = PlaceholderDimensions(
        size: Size(child.getMaxIntrinsicWidth(height), height),
        alignment: _placeholderSpans[childIndex].alignment,
        baseline: _placeholderSpans[childIndex].baseline,
      );
      child = childAfter(child);
      childIndex += 1;
    }
    _textPainter.setPlaceholderDimensions(placeholderDimensions);
  }

  void _computeChildrenWidthWithMinIntrinsics(double height) {
    RenderBox child = firstChild;
    final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>(childCount);
    int childIndex = 0;
    while (child != null) {
      final double intrinsicWidth = child.getMinIntrinsicWidth(height);
      final double intrinsicHeight = child.getMinIntrinsicHeight(intrinsicWidth);
      placeholderDimensions[childIndex] = PlaceholderDimensions(
        size: Size(intrinsicWidth, intrinsicHeight),
        alignment: _placeholderSpans[childIndex].alignment,
        baseline: _placeholderSpans[childIndex].baseline,
      );
      child = childAfter(child);
      childIndex += 1;
    }
    _textPainter.setPlaceholderDimensions(placeholderDimensions);
  }

  void _computeChildrenHeightWithMinIntrinsics(double width) {
    RenderBox child = firstChild;
    final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>(childCount);
    int childIndex = 0;
    while (child != null) {
      final double intrinsicHeight = child.getMinIntrinsicHeight(width);
      final double intrinsicWidth = child.getMinIntrinsicWidth(intrinsicHeight);
      placeholderDimensions[childIndex] = PlaceholderDimensions(
        size: Size(intrinsicWidth, intrinsicHeight),
        alignment: _placeholderSpans[childIndex].alignment,
        baseline: _placeholderSpans[childIndex].baseline,
      );
      child = childAfter(child);
      childIndex += 1;
    }
    _textPainter.setPlaceholderDimensions(placeholderDimensions);
  }

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

438 439 440 441
  @override
  bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
    RenderBox child = firstChild;
    while (child != null) {
442
      final TextParentData textParentData = child.parentData as TextParentData;
443 444 445 446 447 448 449 450 451
      final Matrix4 transform = Matrix4.translationValues(
        textParentData.offset.dx,
        textParentData.offset.dy,
        0.0,
      )..scale(
        textParentData.scale,
        textParentData.scale,
        textParentData.scale,
      );
452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471
      final bool isHit = result.addWithPaintTransform(
        transform: transform,
        position: position,
        hitTest: (BoxHitTestResult result, Offset transformed) {
          assert(() {
            final Offset manualPosition = (position - textParentData.offset) / textParentData.scale;
            return (transformed.dx - manualPosition.dx).abs() < precisionErrorTolerance
              && (transformed.dy - manualPosition.dy).abs() < precisionErrorTolerance;
          }());
          return child.hitTest(result, position: transformed);
        },
      );
      if (isHit) {
        return true;
      }
      child = childAfter(child);
    }
    return false;
  }

472
  @override
473
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
474
    assert(debugHandleEvent(event, entry));
475 476
    if (event is! PointerDownEvent)
      return;
477
    _layoutTextWithConstraints(constraints);
478
    final Offset offset = entry.localPosition;
479
    final TextPosition position = _textPainter.getPositionForOffset(offset);
480 481 482 483 484 485
    final InlineSpan span = _textPainter.text.getSpanForPosition(position);
    if (span == null) {
      return;
    }
    if (span is TextSpan) {
      final TextSpan textSpan = span;
486
      textSpan.recognizer?.addPointer(event as PointerDownEvent);
487
    }
488 489
  }

490
  bool _needsClipping = false;
491 492
  ui.Shader _overflowShader;

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

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

510 511 512 513 514 515
  @override
  void systemFontsDidChange() {
    super.systemFontsDidChange();
    _textPainter.markNeedsLayout();
  }

516 517 518 519 520 521 522
  // 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.
  List<PlaceholderDimensions> _placeholderDimensions;

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

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

593
  @override
594
  void performLayout() {
595
    final BoxConstraints constraints = this.constraints;
596
    _layoutChildren(constraints);
597
    _layoutTextWithConstraints(constraints);
598 599
    _setParentData();

600 601 602 603 604
    // 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.
605
    final Size textSize = _textPainter.size;
606
    final bool textDidExceedMaxLines = _textPainter.didExceedMaxLines;
607
    size = constraints.constrain(textSize);
608

609
    final bool didOverflowHeight = size.height < textSize.height || textDidExceedMaxLines;
610
    final bool didOverflowWidth = size.width < textSize.width;
611 612 613 614 615
    // 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.
616 617
    final bool hasVisualOverflow = didOverflowWidth || didOverflowHeight;
    if (hasVisualOverflow) {
618
      switch (_overflow) {
619 620 621 622
        case TextOverflow.visible:
          _needsClipping = false;
          _overflowShader = null;
          break;
623
        case TextOverflow.clip:
624
        case TextOverflow.ellipsis:
625
          _needsClipping = true;
626 627 628
          _overflowShader = null;
          break;
        case TextOverflow.fade:
Ian Hickson's avatar
Ian Hickson committed
629
          assert(textDirection != null);
630
          _needsClipping = true;
631 632
          final TextPainter fadeSizePainter = TextPainter(
            text: TextSpan(style: _textPainter.text.style, text: '\u2026'),
Ian Hickson's avatar
Ian Hickson committed
633 634
            textDirection: textDirection,
            textScaleFactor: textScaleFactor,
635
            locale: locale,
636
          )..layout();
637
          if (didOverflowWidth) {
Ian Hickson's avatar
Ian Hickson committed
638 639 640 641 642 643 644 645 646 647 648
            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;
            }
649 650 651
            _overflowShader = ui.Gradient.linear(
              Offset(fadeStart, 0.0),
              Offset(fadeEnd, 0.0),
652
              <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
653 654 655 656
            );
          } else {
            final double fadeEnd = size.height;
            final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0;
657 658 659
            _overflowShader = ui.Gradient.linear(
              Offset(0.0, fadeStart),
              Offset(0.0, fadeEnd),
660
              <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
661 662
            );
          }
663 664 665
          break;
      }
    } else {
666
      _needsClipping = false;
667 668
      _overflowShader = null;
    }
669 670
  }

671
  @override
672
  void paint(PaintingContext context, Offset offset) {
673 674
    // Ideally we could compute the min/max intrinsic width/height with a
    // non-destructive operation. However, currently, computing these values
675 676
    // will destroy state inside the painter. If that happens, we need to get
    // back the correct state by calling _layout again.
677
    //
678 679
    // TODO(abarth): Make computing the min/max intrinsic width/height a
    //  non-destructive operation.
680 681 682
    //
    // If you remove this call, make sure that changing the textAlign still
    // works properly.
683
    _layoutTextWithConstraints(constraints);
684 685 686

    assert(() {
      if (debugRepaintTextRainbowEnabled) {
687
        final Paint paint = Paint()
688
          ..color = debugCurrentRepaintColor.toColor();
689
        context.canvas.drawRect(offset & size, paint);
690 691
      }
      return true;
692
    }());
693

694
    if (_needsClipping) {
695
      final Rect bounds = offset & size;
696
      if (_overflowShader != null) {
697 698
        // This layer limits what the shader below blends with to be just the
        // text (as opposed to the text and its background).
699
        context.canvas.saveLayer(bounds, Paint());
700
      } else {
701
        context.canvas.save();
702
      }
703 704 705 706 707 708
      context.canvas.clipRect(bounds);
    }
    _textPainter.paint(context.canvas, offset);

    RenderBox child = firstChild;
    int childIndex = 0;
709 710 711 712 713
    // 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.
    while (child != null && childIndex < _textPainter.inlinePlaceholderBoxes.length) {
714
      final TextParentData textParentData = child.parentData as TextParentData;
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729

      final double scale = textParentData.scale;
      context.pushTransform(
        needsCompositing,
        offset + textParentData.offset,
        Matrix4.diagonal3Values(scale, scale, scale),
        (PaintingContext context, Offset offset) {
          context.paintChild(
            child,
            offset,
          );
        },
      );
      child = childAfter(child);
      childIndex += 1;
730
    }
731
    if (_needsClipping) {
732
      if (_overflowShader != null) {
733
        context.canvas.translate(offset.dx, offset.dy);
734
        final Paint paint = Paint()
735
          ..blendMode = BlendMode.modulate
736
          ..shader = _overflowShader;
737
        context.canvas.drawRect(Offset.zero & size, paint);
738
      }
739
      context.canvas.restore();
740
    }
741 742
  }

743 744 745 746
  /// Returns the offset at which to paint the caret.
  ///
  /// Valid only after [layout].
  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
747
    assert(!debugNeedsLayout);
748 749 750 751 752 753 754 755 756 757 758 759
    _layoutTextWithConstraints(constraints);
    return _textPainter.getOffsetForCaret(position, caretPrototype);
  }

  /// 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) {
760
    assert(!debugNeedsLayout);
761 762 763 764 765 766 767 768
    _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) {
769
    assert(!debugNeedsLayout);
770 771 772 773 774 775 776 777 778 779 780 781 782 783
    _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) {
784
    assert(!debugNeedsLayout);
785 786 787 788
    _layoutTextWithConstraints(constraints);
    return _textPainter.getWordBoundary(position);
  }

789 790 791 792 793 794 795 796 797 798 799 800 801 802
  /// 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;
  }

803 804 805 806 807 808 809 810 811 812 813
  /// Collected during [describeSemanticsConfiguration], used by
  /// [assembleSemanticsNode] and [_combineSemanticsInfo].
  List<InlineSpanSemanticsInformation> _semanticsInfo;

  /// Combines _semanticsInfo entries where permissible, determined by
  /// [InlineSpanSemanticsInformation.requiresOwnNode].
  List<InlineSpanSemanticsInformation> _combineSemanticsInfo() {
    assert(_semanticsInfo != null);
    final List<InlineSpanSemanticsInformation> combined = <InlineSpanSemanticsInformation>[];
    String workingText = '';
    String workingLabel;
814
    for (final InlineSpanSemanticsInformation info in _semanticsInfo) {
815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844
      if (info.requiresOwnNode) {
        if (workingText != null) {
          combined.add(InlineSpanSemanticsInformation(
            workingText,
            semanticsLabel: workingLabel ?? workingText,
          ));
          workingText = '';
          workingLabel = null;
        }
        combined.add(info);
      } else {
        workingText += info.text;
        workingLabel ??= '';
        if (info.semanticsLabel != null) {
          workingLabel += info.semanticsLabel;
        } else {
          workingLabel += info.text;
        }
      }
    }
    if (workingText != null) {
      combined.add(InlineSpanSemanticsInformation(
        workingText,
        semanticsLabel: workingLabel,
      ));
    } else {
      assert(workingLabel != null);
    }
    return combined;
  }
845

846
  @override
847 848
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
849 850 851
    _semanticsInfo = text.getSemanticsInformation();

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

864 865 866 867 868 869
  // 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.
  Queue<SemanticsNode> _cachedChildNodes;

870 871
  @override
  void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
872
    assert(_semanticsInfo != null && _semanticsInfo.isNotEmpty);
873 874 875
    final List<SemanticsNode> newChildren = <SemanticsNode>[];
    TextDirection currentDirection = textDirection;
    Rect currentRect;
876 877 878 879
    double ordinal = 0.0;
    int start = 0;
    int placeholderIndex = 0;
    RenderBox child = firstChild;
880
    final Queue<SemanticsNode> newChildCache = Queue<SemanticsNode>();
881
    for (final InlineSpanSemanticsInformation info in _combineSemanticsInfo()) {
882
      final TextDirection initialDirection = currentDirection;
883 884 885 886
      final TextSelection selection = TextSelection(
        baseOffset: start,
        extentOffset: start + info.text.length,
      );
887
      final List<ui.TextBox> rects = getBoxesForSelection(selection);
888
      if (rects.isEmpty) {
889
        continue;
890 891 892
      }
      Rect rect = rects.first.toRect();
      currentDirection = rects.first.direction;
893
      for (final ui.TextBox textBox in rects.skip(1)) {
894 895 896
        rect = rect.expandToInclude(textBox.toRect());
        currentDirection = textBox.direction;
      }
897 898 899 900 901 902 903 904
      // 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),
      );
905 906
      // round the current rectangle to make this API testable and add some
      // padding so that the accessibility rects do not overlap with the text.
907
      currentRect = Rect.fromLTRB(
908 909 910 911 912 913
        rect.left.floorToDouble() - 4.0,
        rect.top.floorToDouble() - 4.0,
        rect.right.ceilToDouble() + 4.0,
        rect.bottom.ceilToDouble() + 4.0,
      );

914 915
      if (info.isPlaceholder) {
        final SemanticsNode childNode = children.elementAt(placeholderIndex++);
916
        final TextParentData parentData = child.parentData as TextParentData;
917 918 919 920 921 922
        childNode.rect = Rect.fromLTWH(
          childNode.rect.left,
          childNode.rect.top,
          childNode.rect.width * parentData.scale,
          childNode.rect.height * parentData.scale,
        );
923
        newChildren.add(childNode);
924
        child = childAfter(child);
925 926 927 928 929
      } else {
        final SemanticsConfiguration configuration = SemanticsConfiguration()
          ..sortKey = OrdinalSortKey(ordinal++)
          ..textDirection = initialDirection
          ..label = info.semanticsLabel ?? info.text;
930 931 932
        final GestureRecognizer recognizer = info.recognizer;
        if (recognizer != null) {
          if (recognizer is TapGestureRecognizer) {
933
            configuration.onTap = recognizer.onTap;
934
            configuration.isLink = true;
935 936 937
          } else if (recognizer is DoubleTapGestureRecognizer) {
            configuration.onTap = recognizer.onDoubleTap;
            configuration.isLink = true;
938
          } else if (recognizer is LongPressGestureRecognizer) {
939 940
            configuration.onLongPress = recognizer.onLongPress;
          } else {
941
            assert(false, '${recognizer.runtimeType} is not supported.');
942 943
          }
        }
944 945 946 947 948 949 950 951
        final SemanticsNode newChild = (_cachedChildNodes?.isNotEmpty == true)
            ? _cachedChildNodes.removeFirst()
            : SemanticsNode();
        newChild
          ..updateWith(config: configuration)
          ..rect = currentRect;
        newChildCache.addLast(newChild);
        newChildren.add(newChild);
952
      }
953
      start += info.text.length;
954
    }
955
    _cachedChildNodes = newChildCache;
956
    node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
Hixie's avatar
Hixie committed
957
  }
958

959 960 961 962 963 964
  @override
  void clearSemantics() {
    super.clearSemantics();
    _cachedChildNodes = null;
  }

965
  @override
966
  List<DiagnosticsNode> debugDescribeChildren() {
967 968 969 970 971 972
    return <DiagnosticsNode>[
      text.toDiagnosticsNode(
        name: 'text',
        style: DiagnosticsTreeStyle.transition,
      )
    ];
973
  }
Ian Hickson's avatar
Ian Hickson committed
974 975

  @override
976 977
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
978 979
    properties.add(EnumProperty<TextAlign>('textAlign', textAlign));
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
980 981 982 983 984 985 986 987 988
    properties.add(
      FlagProperty(
        'softWrap',
        value: softWrap,
        ifTrue: 'wrapping at box width',
        ifFalse: 'no wrapping except at line break characters',
        showName: true,
      )
    );
989
    properties.add(EnumProperty<TextOverflow>('overflow', overflow));
990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003
    properties.add(
      DoubleProperty(
        'textScaleFactor',
        textScaleFactor,
        defaultValue: 1.0,
      )
    );
    properties.add(
      DiagnosticsProperty<Locale>(
        'locale',
        locale,
        defaultValue: null,
      )
    );
1004
    properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
Ian Hickson's avatar
Ian Hickson committed
1005
  }
1006
}