paragraph.dart 15.1 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.

Ian Hickson's avatar
Ian Hickson committed
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/semantics.dart';
10
import 'package:flutter/services.dart';
11

12

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

17 18 19 20 21 22 23 24 25 26 27 28
/// 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,
}

29 30
const String _kEllipsis = '\u2026';

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

66
  final TextPainter _textPainter;
67

68
  /// The text to display
69
  TextSpan get text => _textPainter.text;
70
  set text(TextSpan value) {
71
    assert(value != null);
72 73 74 75 76 77 78 79 80 81 82 83 84 85
    switch (_textPainter.text.compareTo(value)) {
      case RenderComparison.identical:
      case RenderComparison.metadata:
        return;
      case RenderComparison.paint:
        _textPainter.text = value;
        markNeedsPaint();
        break;
      case RenderComparison.layout:
        _textPainter.text = value;
        _overflowShader = null;
        markNeedsLayout();
        break;
    }
86 87
  }

88 89
  /// How the text should be aligned horizontally.
  TextAlign get textAlign => _textPainter.textAlign;
90
  set textAlign(TextAlign value) {
Ian Hickson's avatar
Ian Hickson committed
91
    assert(value != null);
92 93 94 95 96 97
    if (_textPainter.textAlign == value)
      return;
    _textPainter.textAlign = value;
    markNeedsPaint();
  }

Ian Hickson's avatar
Ian Hickson committed
98 99 100 101 102 103 104 105 106
  /// 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]
107
  /// context, the English phrase will be on the right and the Hebrew phrase on
Ian Hickson's avatar
Ian Hickson committed
108 109 110 111 112 113 114 115 116 117 118 119
  /// 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();
  }

120 121
  /// Whether the text should break at soft line breaks.
  ///
122 123 124 125 126
  /// 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.
127 128
  bool get softWrap => _softWrap;
  bool _softWrap;
129
  set softWrap(bool value) {
130 131 132 133 134 135 136 137 138 139
    assert(value != null);
    if (_softWrap == value)
      return;
    _softWrap = value;
    markNeedsLayout();
  }

  /// How visual overflow should be handled.
  TextOverflow get overflow => _overflow;
  TextOverflow _overflow;
140
  set overflow(TextOverflow value) {
141 142 143 144
    assert(value != null);
    if (_overflow == value)
      return;
    _overflow = value;
145
    _textPainter.ellipsis = value == TextOverflow.ellipsis ? _kEllipsis : null;
146
    markNeedsLayout();
147 148
  }

149 150 151 152 153 154 155 156 157 158 159 160 161 162
  /// 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();
  }

163 164
  /// 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
165
  /// to [overflow] and [softWrap].
166
  int get maxLines => _textPainter.maxLines;
167
  /// The value may be null. If it is not null, then it must be greater than zero.
168
  set maxLines(int value) {
169
    assert(value == null || value > 0);
170 171 172 173 174 175 176
    if (_textPainter.maxLines == value)
      return;
    _textPainter.maxLines = value;
    _overflowShader = null;
    markNeedsLayout();
  }

177
  void _layoutText({ double minWidth: 0.0, double maxWidth: double.INFINITY }) {
Ian Hickson's avatar
Ian Hickson committed
178 179
    final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
    _textPainter.layout(minWidth: minWidth, maxWidth: widthMatters ? maxWidth : double.INFINITY);
180 181
  }

182 183 184 185
  void _layoutTextWithConstraints(BoxConstraints constraints) {
    _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
  }

186
  @override
187
  double computeMinIntrinsicWidth(double height) {
188 189
    _layoutText();
    return _textPainter.minIntrinsicWidth;
190 191
  }

192
  @override
193
  double computeMaxIntrinsicWidth(double height) {
194 195
    _layoutText();
    return _textPainter.maxIntrinsicWidth;
196 197
  }

198
  double _computeIntrinsicHeight(double width) {
199 200
    _layoutText(minWidth: width, maxWidth: width);
    return _textPainter.height;
201 202
  }

203
  @override
204 205
  double computeMinIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
206 207
  }

208
  @override
209 210
  double computeMaxIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
211 212
  }

213
  @override
214
  double computeDistanceToActualBaseline(TextBaseline baseline) {
215
    assert(!debugNeedsLayout);
216 217
    assert(constraints != null);
    assert(constraints.debugAssertIsValid());
218
    _layoutTextWithConstraints(constraints);
219
    return _textPainter.computeDistanceToActualBaseline(baseline);
220 221
  }

222
  @override
223
  bool hitTestSelf(Offset position) => true;
Adam Barth's avatar
Adam Barth committed
224

225
  @override
226
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
227
    assert(debugHandleEvent(event, entry));
228 229
    if (event is! PointerDownEvent)
      return;
230
    _layoutTextWithConstraints(constraints);
231
    final Offset offset = entry.localPosition;
232 233
    final TextPosition position = _textPainter.getPositionForOffset(offset);
    final TextSpan span = _textPainter.text.getSpanForPosition(position);
234 235 236
    span?.recognizer?.addPointer(event);
  }

237 238 239
  bool _hasVisualOverflow = false;
  ui.Shader _overflowShader;

240
  /// Whether this paragraph currently has a [dart:ui.Shader] for its overflow
Ian Hickson's avatar
Ian Hickson committed
241
  /// effect.
242 243
  ///
  /// Used to test this object. Not for use in production.
244 245 246
  @visibleForTesting
  bool get debugHasOverflowShader => _overflowShader != null;

247
  @override
248
  void performLayout() {
249
    _layoutTextWithConstraints(constraints);
250 251 252
    // We grab _textPainter.size 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.
253
    // Other _textPainter state like didExceedMaxLines will also be affected.
254
    // See also RenderEditable which has a similar issue.
255
    final Size textSize = _textPainter.size;
256
    final bool didOverflowHeight = _textPainter.didExceedMaxLines;
257
    size = constraints.constrain(textSize);
258

259
    final bool didOverflowWidth = size.width < textSize.width;
260 261 262 263 264
    // 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.
265 266
    _hasVisualOverflow = didOverflowWidth || didOverflowHeight;
    if (_hasVisualOverflow) {
267 268
      switch (_overflow) {
        case TextOverflow.clip:
269
        case TextOverflow.ellipsis:
270 271 272
          _overflowShader = null;
          break;
        case TextOverflow.fade:
Ian Hickson's avatar
Ian Hickson committed
273
          assert(textDirection != null);
274
          final TextPainter fadeSizePainter = new TextPainter(
275
            text: new TextSpan(style: _textPainter.text.style, text: '\u2026'),
Ian Hickson's avatar
Ian Hickson committed
276 277
            textDirection: textDirection,
            textScaleFactor: textScaleFactor,
278
          )..layout();
279
          if (didOverflowWidth) {
Ian Hickson's avatar
Ian Hickson committed
280 281 282 283 284 285 286 287 288 289 290
            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;
            }
291
            _overflowShader = new ui.Gradient.linear(
292 293 294
              new Offset(fadeStart, 0.0),
              new Offset(fadeEnd, 0.0),
              <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
295 296 297 298 299
            );
          } else {
            final double fadeEnd = size.height;
            final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0;
            _overflowShader = new ui.Gradient.linear(
300 301 302
              new Offset(0.0, fadeStart),
              new Offset(0.0, fadeEnd),
              <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
303 304
            );
          }
305 306 307 308 309
          break;
      }
    } else {
      _overflowShader = null;
    }
310 311
  }

312
  @override
313
  void paint(PaintingContext context, Offset offset) {
314 315
    // Ideally we could compute the min/max intrinsic width/height with a
    // non-destructive operation. However, currently, computing these values
316
    // will destroy state inside the painter. If that happens, we need to
317 318 319 320
    // get back the correct state by calling _layout again.
    //
    // TODO(abarth): Make computing the min/max intrinsic width/height
    // a non-destructive operation.
321 322 323
    //
    // If you remove this call, make sure that changing the textAlign still
    // works properly.
324
    _layoutTextWithConstraints(constraints);
325
    final Canvas canvas = context.canvas;
326 327 328

    assert(() {
      if (debugRepaintTextRainbowEnabled) {
329
        final Paint paint = new Paint()
330 331 332 333
          ..color = debugCurrentRepaintColor.toColor();
        canvas.drawRect(offset & size, paint);
      }
      return true;
334
    }());
335

336 337
    if (_hasVisualOverflow) {
      final Rect bounds = offset & size;
338 339 340
      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).
341
        canvas.saveLayer(bounds, new Paint());
342
      } else {
343
        canvas.save();
344
      }
345 346 347 348 349 350
      canvas.clipRect(bounds);
    }
    _textPainter.paint(canvas, offset);
    if (_hasVisualOverflow) {
      if (_overflowShader != null) {
        canvas.translate(offset.dx, offset.dy);
351
        final Paint paint = new Paint()
352
          ..blendMode = BlendMode.modulate
353
          ..shader = _overflowShader;
354
        canvas.drawRect(Offset.zero & size, paint);
355 356 357
      }
      canvas.restore();
    }
358 359
  }

360 361 362 363
  /// Returns the offset at which to paint the caret.
  ///
  /// Valid only after [layout].
  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
364
    assert(!debugNeedsLayout);
365 366 367 368 369 370 371 372 373 374 375 376
    _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) {
377
    assert(!debugNeedsLayout);
378 379 380 381 382 383 384 385
    _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) {
386
    assert(!debugNeedsLayout);
387 388 389 390 391 392 393 394 395 396 397 398 399 400
    _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) {
401
    assert(!debugNeedsLayout);
402 403 404 405
    _layoutTextWithConstraints(constraints);
    return _textPainter.getWordBoundary(position);
  }

406 407 408 409 410 411 412 413 414 415 416 417 418 419
  /// 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;
  }

420
  @override
421 422 423 424 425
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
    config
      ..label = text.toPlainText()
      ..textDirection = textDirection;
Hixie's avatar
Hixie committed
426
  }
427

428
  @override
429 430
  List<DiagnosticsNode> debugDescribeChildren() {
    return <DiagnosticsNode>[text.toDiagnosticsNode(name: 'text', style: DiagnosticsTreeStyle.transition)];
431
  }
Ian Hickson's avatar
Ian Hickson committed
432 433 434 435 436 437 438 439 440 441 442

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
    super.debugFillProperties(description);
    description.add(new EnumProperty<TextAlign>('textAlign', textAlign));
    description.add(new EnumProperty<TextDirection>('textDirection', textDirection));
    description.add(new FlagProperty('softWrap', value: softWrap, ifTrue: 'wrapping at box width', ifFalse: 'no wrapping except at line break characters', showName: true));
    description.add(new EnumProperty<TextOverflow>('overflow', overflow));
    description.add(new DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: 1.0));
    description.add(new IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
  }
443
}