paragraph.dart 20.4 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:ui' as ui show Gradient, Shader, TextBox;
6

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

13

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

18 19 20 21 22 23 24 25 26 27
/// How overflowing text should be handled.
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,
28 29 30

  /// Render overflowing text outside of its container.
  visible,
31 32
}

33 34
const String _kEllipsis = '\u2026';

35
/// A render object that displays a paragraph of text
36
class RenderParagraph extends RenderBox {
37 38
  /// Creates a paragraph render object.
  ///
Ian Hickson's avatar
Ian Hickson committed
39 40
  /// The [text], [textAlign], [textDirection], [overflow], [softWrap], and
  /// [textScaleFactor] arguments must not be null.
41 42 43
  ///
  /// The [maxLines] property may be null (and indeed defaults to null), but if
  /// it is not null, it must be greater than zero.
44
  RenderParagraph(TextSpan text, {
45
    TextAlign textAlign = TextAlign.start,
Ian Hickson's avatar
Ian Hickson committed
46
    @required TextDirection textDirection,
47 48 49
    bool softWrap = true,
    TextOverflow overflow = TextOverflow.clip,
    double textScaleFactor = 1.0,
50
    int maxLines,
51
    Locale locale,
52
    StrutStyle strutStyle,
53 54
  }) : assert(text != null),
       assert(text.debugAssertIsValid()),
Ian Hickson's avatar
Ian Hickson committed
55 56
       assert(textAlign != null),
       assert(textDirection != null),
57 58 59
       assert(softWrap != null),
       assert(overflow != null),
       assert(textScaleFactor != null),
60
       assert(maxLines == null || maxLines > 0),
61
       _softWrap = softWrap,
62
       _overflow = overflow,
63
       _textPainter = TextPainter(
64 65
         text: text,
         textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
66
         textDirection: textDirection,
67 68 69
         textScaleFactor: textScaleFactor,
         maxLines: maxLines,
         ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
70
         locale: locale,
71
         strutStyle: strutStyle,
72
       );
73

74
  final TextPainter _textPainter;
75

76
  /// The text to display
77
  TextSpan get text => _textPainter.text;
78
  set text(TextSpan value) {
79
    assert(value != null);
80 81 82 83 84 85 86
    switch (_textPainter.text.compareTo(value)) {
      case RenderComparison.identical:
      case RenderComparison.metadata:
        return;
      case RenderComparison.paint:
        _textPainter.text = value;
        markNeedsPaint();
87
        markNeedsSemanticsUpdate();
88 89 90 91 92 93 94
        break;
      case RenderComparison.layout:
        _textPainter.text = value;
        _overflowShader = null;
        markNeedsLayout();
        break;
    }
95 96
  }

97 98
  /// How the text should be aligned horizontally.
  TextAlign get textAlign => _textPainter.textAlign;
99
  set textAlign(TextAlign value) {
Ian Hickson's avatar
Ian Hickson committed
100
    assert(value != null);
101 102 103 104 105 106
    if (_textPainter.textAlign == value)
      return;
    _textPainter.textAlign = value;
    markNeedsPaint();
  }

Ian Hickson's avatar
Ian Hickson committed
107 108 109 110 111 112 113 114 115
  /// 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]
116
  /// context, the English phrase will be on the right and the Hebrew phrase on
Ian Hickson's avatar
Ian Hickson committed
117 118 119 120 121 122 123 124 125 126 127 128
  /// 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();
  }

129 130
  /// Whether the text should break at soft line breaks.
  ///
131 132 133 134 135
  /// 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.
136 137
  bool get softWrap => _softWrap;
  bool _softWrap;
138
  set softWrap(bool value) {
139 140 141 142 143 144 145 146 147 148
    assert(value != null);
    if (_softWrap == value)
      return;
    _softWrap = value;
    markNeedsLayout();
  }

  /// How visual overflow should be handled.
  TextOverflow get overflow => _overflow;
  TextOverflow _overflow;
149
  set overflow(TextOverflow value) {
150 151 152 153
    assert(value != null);
    if (_overflow == value)
      return;
    _overflow = value;
154
    _textPainter.ellipsis = value == TextOverflow.ellipsis ? _kEllipsis : null;
155
    markNeedsLayout();
156 157
  }

158 159 160 161 162 163 164 165 166 167 168 169 170 171
  /// 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();
  }

172 173
  /// 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
174
  /// to [overflow] and [softWrap].
175
  int get maxLines => _textPainter.maxLines;
176
  /// The value may be null. If it is not null, then it must be greater than zero.
177
  set maxLines(int value) {
178
    assert(value == null || value > 0);
179 180 181 182 183 184 185
    if (_textPainter.maxLines == value)
      return;
    _textPainter.maxLines = value;
    _overflowShader = null;
    markNeedsLayout();
  }

186 187 188 189 190 191 192 193 194 195
  /// Used by this paragraph's internal [TextPainter] to select a locale-specific
  /// font.
  ///
  /// 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.
  Locale get locale => _textPainter.locale;
  /// The value may be null.
  set locale(Locale value) {
196 197
    if (_textPainter.locale == value)
      return;
198
    _textPainter.locale = value;
199 200 201 202
    _overflowShader = null;
    markNeedsLayout();
  }

203 204 205 206 207 208 209 210 211 212 213
  /// {@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();
  }

214
  void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
Ian Hickson's avatar
Ian Hickson committed
215
    final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
216
    _textPainter.layout(minWidth: minWidth, maxWidth: widthMatters ? maxWidth : double.infinity);
217 218
  }

219 220 221 222
  void _layoutTextWithConstraints(BoxConstraints constraints) {
    _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
  }

223
  @override
224
  double computeMinIntrinsicWidth(double height) {
225 226
    _layoutText();
    return _textPainter.minIntrinsicWidth;
227 228
  }

229
  @override
230
  double computeMaxIntrinsicWidth(double height) {
231 232
    _layoutText();
    return _textPainter.maxIntrinsicWidth;
233 234
  }

235
  double _computeIntrinsicHeight(double width) {
236 237
    _layoutText(minWidth: width, maxWidth: width);
    return _textPainter.height;
238 239
  }

240
  @override
241 242
  double computeMinIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
243 244
  }

245
  @override
246 247
  double computeMaxIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
248 249
  }

250
  @override
251
  double computeDistanceToActualBaseline(TextBaseline baseline) {
252
    assert(!debugNeedsLayout);
253 254
    assert(constraints != null);
    assert(constraints.debugAssertIsValid());
255
    _layoutTextWithConstraints(constraints);
256
    return _textPainter.computeDistanceToActualBaseline(baseline);
257 258
  }

259
  @override
260
  bool hitTestSelf(Offset position) => true;
Adam Barth's avatar
Adam Barth committed
261

262
  @override
263
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
264
    assert(debugHandleEvent(event, entry));
265 266
    if (event is! PointerDownEvent)
      return;
267
    _layoutTextWithConstraints(constraints);
268
    final Offset offset = entry.localPosition;
269 270
    final TextPosition position = _textPainter.getPositionForOffset(offset);
    final TextSpan span = _textPainter.text.getSpanForPosition(position);
271 272 273
    span?.recognizer?.addPointer(event);
  }

274
  bool _needsClipping = false;
275 276
  ui.Shader _overflowShader;

277
  /// Whether this paragraph currently has a [dart:ui.Shader] for its overflow
Ian Hickson's avatar
Ian Hickson committed
278
  /// effect.
279 280
  ///
  /// Used to test this object. Not for use in production.
281 282 283
  @visibleForTesting
  bool get debugHasOverflowShader => _overflowShader != null;

284
  @override
285
  void performLayout() {
286
    _layoutTextWithConstraints(constraints);
287 288 289 290 291
    // 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.
292
    final Size textSize = _textPainter.size;
293
    final bool textDidExceedMaxLines = _textPainter.didExceedMaxLines;
294
    size = constraints.constrain(textSize);
295

296
    final bool didOverflowHeight = size.height < textSize.height || textDidExceedMaxLines;
297
    final bool didOverflowWidth = size.width < textSize.width;
298 299 300 301 302
    // 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.
303 304
    final bool hasVisualOverflow = didOverflowWidth || didOverflowHeight;
    if (hasVisualOverflow) {
305
      switch (_overflow) {
306 307 308 309
        case TextOverflow.visible:
          _needsClipping = false;
          _overflowShader = null;
          break;
310
        case TextOverflow.clip:
311
        case TextOverflow.ellipsis:
312
          _needsClipping = true;
313 314 315
          _overflowShader = null;
          break;
        case TextOverflow.fade:
Ian Hickson's avatar
Ian Hickson committed
316
          assert(textDirection != null);
317
          _needsClipping = true;
318 319
          final TextPainter fadeSizePainter = TextPainter(
            text: TextSpan(style: _textPainter.text.style, text: '\u2026'),
Ian Hickson's avatar
Ian Hickson committed
320 321
            textDirection: textDirection,
            textScaleFactor: textScaleFactor,
322
            locale: locale,
323
          )..layout();
324
          if (didOverflowWidth) {
Ian Hickson's avatar
Ian Hickson committed
325 326 327 328 329 330 331 332 333 334 335
            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;
            }
336 337 338
            _overflowShader = ui.Gradient.linear(
              Offset(fadeStart, 0.0),
              Offset(fadeEnd, 0.0),
339
              <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
340 341 342 343
            );
          } else {
            final double fadeEnd = size.height;
            final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0;
344 345 346
            _overflowShader = ui.Gradient.linear(
              Offset(0.0, fadeStart),
              Offset(0.0, fadeEnd),
347
              <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
348 349
            );
          }
350 351 352
          break;
      }
    } else {
353
      _needsClipping = false;
354 355
      _overflowShader = null;
    }
356 357
  }

358
  @override
359
  void paint(PaintingContext context, Offset offset) {
360 361
    // Ideally we could compute the min/max intrinsic width/height with a
    // non-destructive operation. However, currently, computing these values
362
    // will destroy state inside the painter. If that happens, we need to
363 364 365 366
    // get back the correct state by calling _layout again.
    //
    // TODO(abarth): Make computing the min/max intrinsic width/height
    // a non-destructive operation.
367 368 369
    //
    // If you remove this call, make sure that changing the textAlign still
    // works properly.
370
    _layoutTextWithConstraints(constraints);
371
    final Canvas canvas = context.canvas;
372 373 374

    assert(() {
      if (debugRepaintTextRainbowEnabled) {
375
        final Paint paint = Paint()
376 377 378 379
          ..color = debugCurrentRepaintColor.toColor();
        canvas.drawRect(offset & size, paint);
      }
      return true;
380
    }());
381

382
    if (_needsClipping) {
383
      final Rect bounds = offset & size;
384 385 386
      if (_overflowShader != null) {
        // This layer limits what the shader below blends with to be just the text
        // (as opposed to the text and its background).
387
        canvas.saveLayer(bounds, Paint());
388
      } else {
389
        canvas.save();
390
      }
391 392 393
      canvas.clipRect(bounds);
    }
    _textPainter.paint(canvas, offset);
394
    if (_needsClipping) {
395 396
      if (_overflowShader != null) {
        canvas.translate(offset.dx, offset.dy);
397
        final Paint paint = Paint()
398
          ..blendMode = BlendMode.modulate
399
          ..shader = _overflowShader;
400
        canvas.drawRect(Offset.zero & size, paint);
401 402 403
      }
      canvas.restore();
    }
404 405
  }

406 407 408 409
  /// Returns the offset at which to paint the caret.
  ///
  /// Valid only after [layout].
  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
410
    assert(!debugNeedsLayout);
411 412 413 414 415 416 417 418 419 420 421 422
    _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) {
423
    assert(!debugNeedsLayout);
424 425 426 427 428 429 430 431
    _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) {
432
    assert(!debugNeedsLayout);
433 434 435 436 437 438 439 440 441 442 443 444 445 446
    _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) {
447
    assert(!debugNeedsLayout);
448 449 450 451
    _layoutTextWithConstraints(constraints);
    return _textPainter.getWordBoundary(position);
  }

452 453 454 455 456 457 458 459 460 461 462 463 464 465
  /// 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;
  }

466 467 468
  final List<int> _recognizerOffsets = <int>[];
  final List<GestureRecognizer> _recognizers = <GestureRecognizer>[];

469
  @override
470 471
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
    _recognizerOffsets.clear();
    _recognizers.clear();
    int offset = 0;
    text.visitTextSpan((TextSpan span) {
      if (span.recognizer != null && (span.recognizer is TapGestureRecognizer || span.recognizer is LongPressGestureRecognizer)) {
        _recognizerOffsets.add(offset);
        _recognizerOffsets.add(offset + span.text.length);
        _recognizers.add(span.recognizer);
      }
      offset += span.text.length;
      return true;
    });
    if (_recognizerOffsets.isNotEmpty) {
      config.explicitChildNodes = true;
      config.isSemanticBoundary = true;
    } else {
      config.label = text.toPlainText();
      config.textDirection = textDirection;
    }
  }

  @override
  void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
    assert(_recognizerOffsets.isNotEmpty);
    assert(_recognizerOffsets.length.isEven);
    assert(_recognizers.isNotEmpty);
    assert(children.isEmpty);
    final List<SemanticsNode> newChildren = <SemanticsNode>[];
    final String rawLabel = text.toPlainText();
    int current = 0;
    double order = -1.0;
    TextDirection currentDirection = textDirection;
    Rect currentRect;

    SemanticsConfiguration buildSemanticsConfig(int start, int end) {
      final TextDirection initialDirection = currentDirection;
508
      final TextSelection selection = TextSelection(baseOffset: start, extentOffset: end);
509 510 511 512 513 514 515 516 517 518
      final List<ui.TextBox> rects = getBoxesForSelection(selection);
      Rect rect;
      for (ui.TextBox textBox in rects) {
        rect ??= textBox.toRect();
        rect = rect.expandToInclude(textBox.toRect());
        currentDirection = textBox.direction;
      }
      // round the current rectangle to make this API testable and add some
      // padding so that the accessibility rects do not overlap with the text.
      // TODO(jonahwilliams): implement this for all text accessibility rects.
519
      currentRect = Rect.fromLTRB(
520 521 522 523 524 525
        rect.left.floorToDouble() - 4.0,
        rect.top.floorToDouble() - 4.0,
        rect.right.ceilToDouble() + 4.0,
        rect.bottom.ceilToDouble() + 4.0,
      );
      order += 1;
526 527
      return SemanticsConfiguration()
        ..sortKey = OrdinalSortKey(order)
528 529 530 531 532 533 534 535
        ..textDirection = initialDirection
        ..label = rawLabel.substring(start, end);
    }

    for (int i = 0, j = 0; i < _recognizerOffsets.length; i += 2, j++) {
      final int start = _recognizerOffsets[i];
      final int end = _recognizerOffsets[i + 1];
      if (current != start) {
536
        final SemanticsNode node = SemanticsNode();
537 538 539 540 541
        final SemanticsConfiguration configuration = buildSemanticsConfig(current, start);
        node.updateWith(config: configuration);
        node.rect = currentRect;
        newChildren.add(node);
      }
542
      final SemanticsNode node = SemanticsNode();
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557
      final SemanticsConfiguration configuration = buildSemanticsConfig(start, end);
      final GestureRecognizer recognizer = _recognizers[j];
      if (recognizer is TapGestureRecognizer) {
        configuration.onTap = recognizer.onTap;
      } else if (recognizer is LongPressGestureRecognizer) {
        configuration.onLongPress = recognizer.onLongPress;
      } else {
        assert(false);
      }
      node.updateWith(config: configuration);
      node.rect = currentRect;
      newChildren.add(node);
      current = end;
    }
    if (current < rawLabel.length) {
558
      final SemanticsNode node = SemanticsNode();
559 560 561 562 563 564
      final SemanticsConfiguration configuration = buildSemanticsConfig(current, rawLabel.length);
      node.updateWith(config: configuration);
      node.rect = currentRect;
      newChildren.add(node);
    }
    node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
Hixie's avatar
Hixie committed
565
  }
566

567
  @override
568 569
  List<DiagnosticsNode> debugDescribeChildren() {
    return <DiagnosticsNode>[text.toDiagnosticsNode(name: 'text', style: DiagnosticsTreeStyle.transition)];
570
  }
Ian Hickson's avatar
Ian Hickson committed
571 572

  @override
573 574
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
575 576 577 578 579 580 581
    properties.add(EnumProperty<TextAlign>('textAlign', textAlign));
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
    properties.add(FlagProperty('softWrap', value: softWrap, ifTrue: 'wrapping at box width', ifFalse: 'no wrapping except at line break characters', showName: true));
    properties.add(EnumProperty<TextOverflow>('overflow', overflow));
    properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: 1.0));
    properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
    properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
Ian Hickson's avatar
Ian Hickson committed
582
  }
583
}