paragraph.dart 15.9 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/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, {
41
    TextAlign textAlign = TextAlign.start,
Ian Hickson's avatar
Ian Hickson committed
42
    @required TextDirection textDirection,
43 44 45
    bool softWrap = true,
    TextOverflow overflow = TextOverflow.clip,
    double textScaleFactor = 1.0,
46
    int maxLines,
47
    Locale locale,
48 49
  }) : assert(text != null),
       assert(text.debugAssertIsValid()),
Ian Hickson's avatar
Ian Hickson committed
50 51
       assert(textAlign != null),
       assert(textDirection != null),
52 53 54
       assert(softWrap != null),
       assert(overflow != null),
       assert(textScaleFactor != null),
55
       assert(maxLines == null || maxLines > 0),
56
       _softWrap = softWrap,
57
       _overflow = overflow,
58
       _textPainter = new TextPainter(
59 60
         text: text,
         textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
61
         textDirection: textDirection,
62 63 64
         textScaleFactor: textScaleFactor,
         maxLines: maxLines,
         ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
65
         locale: locale,
66
       );
67

68
  final TextPainter _textPainter;
69

70
  /// The text to display
71
  TextSpan get text => _textPainter.text;
72
  set text(TextSpan value) {
73
    assert(value != null);
74 75 76 77 78 79 80 81 82 83 84 85 86 87
    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;
    }
88 89
  }

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

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

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

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

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

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

179 180 181 182 183 184 185 186 187 188
  /// 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) {
189 190
    if (_textPainter.locale == value)
      return;
191
    _textPainter.locale = value;
192 193 194 195
    _overflowShader = null;
    markNeedsLayout();
  }

196
  void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
Ian Hickson's avatar
Ian Hickson committed
197
    final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
198
    _textPainter.layout(minWidth: minWidth, maxWidth: widthMatters ? maxWidth : double.infinity);
199 200
  }

201 202 203 204
  void _layoutTextWithConstraints(BoxConstraints constraints) {
    _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
  }

205
  @override
206
  double computeMinIntrinsicWidth(double height) {
207 208
    _layoutText();
    return _textPainter.minIntrinsicWidth;
209 210
  }

211
  @override
212
  double computeMaxIntrinsicWidth(double height) {
213 214
    _layoutText();
    return _textPainter.maxIntrinsicWidth;
215 216
  }

217
  double _computeIntrinsicHeight(double width) {
218 219
    _layoutText(minWidth: width, maxWidth: width);
    return _textPainter.height;
220 221
  }

222
  @override
223 224
  double computeMinIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
225 226
  }

227
  @override
228 229
  double computeMaxIntrinsicHeight(double width) {
    return _computeIntrinsicHeight(width);
230 231
  }

232
  @override
233
  double computeDistanceToActualBaseline(TextBaseline baseline) {
234
    assert(!debugNeedsLayout);
235 236
    assert(constraints != null);
    assert(constraints.debugAssertIsValid());
237
    _layoutTextWithConstraints(constraints);
238
    return _textPainter.computeDistanceToActualBaseline(baseline);
239 240
  }

241
  @override
242
  bool hitTestSelf(Offset position) => true;
Adam Barth's avatar
Adam Barth committed
243

244
  @override
245
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
246
    assert(debugHandleEvent(event, entry));
247 248
    if (event is! PointerDownEvent)
      return;
249
    _layoutTextWithConstraints(constraints);
250
    final Offset offset = entry.localPosition;
251 252
    final TextPosition position = _textPainter.getPositionForOffset(offset);
    final TextSpan span = _textPainter.text.getSpanForPosition(position);
253 254 255
    span?.recognizer?.addPointer(event);
  }

256 257 258
  bool _hasVisualOverflow = false;
  ui.Shader _overflowShader;

259
  /// Whether this paragraph currently has a [dart:ui.Shader] for its overflow
Ian Hickson's avatar
Ian Hickson committed
260
  /// effect.
261 262
  ///
  /// Used to test this object. Not for use in production.
263 264 265
  @visibleForTesting
  bool get debugHasOverflowShader => _overflowShader != null;

266
  @override
267
  void performLayout() {
268
    _layoutTextWithConstraints(constraints);
269 270 271
    // 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.
272
    // Other _textPainter state like didExceedMaxLines will also be affected.
273
    // See also RenderEditable which has a similar issue.
274
    final Size textSize = _textPainter.size;
275
    final bool didOverflowHeight = _textPainter.didExceedMaxLines;
276
    size = constraints.constrain(textSize);
277

278
    final bool didOverflowWidth = size.width < textSize.width;
279 280 281 282 283
    // 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.
284 285
    _hasVisualOverflow = didOverflowWidth || didOverflowHeight;
    if (_hasVisualOverflow) {
286 287
      switch (_overflow) {
        case TextOverflow.clip:
288
        case TextOverflow.ellipsis:
289 290 291
          _overflowShader = null;
          break;
        case TextOverflow.fade:
Ian Hickson's avatar
Ian Hickson committed
292
          assert(textDirection != null);
293
          final TextPainter fadeSizePainter = new TextPainter(
294
            text: new TextSpan(style: _textPainter.text.style, text: '\u2026'),
Ian Hickson's avatar
Ian Hickson committed
295 296
            textDirection: textDirection,
            textScaleFactor: textScaleFactor,
297
            locale: locale,
298
          )..layout();
299
          if (didOverflowWidth) {
Ian Hickson's avatar
Ian Hickson committed
300 301 302 303 304 305 306 307 308 309 310
            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;
            }
311
            _overflowShader = new ui.Gradient.linear(
312 313 314
              new Offset(fadeStart, 0.0),
              new Offset(fadeEnd, 0.0),
              <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
315 316 317 318 319
            );
          } else {
            final double fadeEnd = size.height;
            final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0;
            _overflowShader = new ui.Gradient.linear(
320 321 322
              new Offset(0.0, fadeStart),
              new Offset(0.0, fadeEnd),
              <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
323 324
            );
          }
325 326 327 328 329
          break;
      }
    } else {
      _overflowShader = null;
    }
330 331
  }

332
  @override
333
  void paint(PaintingContext context, Offset offset) {
334 335
    // Ideally we could compute the min/max intrinsic width/height with a
    // non-destructive operation. However, currently, computing these values
336
    // will destroy state inside the painter. If that happens, we need to
337 338 339 340
    // get back the correct state by calling _layout again.
    //
    // TODO(abarth): Make computing the min/max intrinsic width/height
    // a non-destructive operation.
341 342 343
    //
    // If you remove this call, make sure that changing the textAlign still
    // works properly.
344
    _layoutTextWithConstraints(constraints);
345
    final Canvas canvas = context.canvas;
346 347 348

    assert(() {
      if (debugRepaintTextRainbowEnabled) {
349
        final Paint paint = new Paint()
350 351 352 353
          ..color = debugCurrentRepaintColor.toColor();
        canvas.drawRect(offset & size, paint);
      }
      return true;
354
    }());
355

356 357
    if (_hasVisualOverflow) {
      final Rect bounds = offset & size;
358 359 360 361 362
      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).
        canvas.saveLayer(bounds, new Paint());
      } else {
363
        canvas.save();
364
      }
365 366 367 368 369 370
      canvas.clipRect(bounds);
    }
    _textPainter.paint(canvas, offset);
    if (_hasVisualOverflow) {
      if (_overflowShader != null) {
        canvas.translate(offset.dx, offset.dy);
371
        final Paint paint = new Paint()
372
          ..blendMode = BlendMode.modulate
373
          ..shader = _overflowShader;
374
        canvas.drawRect(Offset.zero & size, paint);
375 376 377
      }
      canvas.restore();
    }
378 379
  }

380 381 382 383
  /// Returns the offset at which to paint the caret.
  ///
  /// Valid only after [layout].
  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
384
    assert(!debugNeedsLayout);
385 386 387 388 389 390 391 392 393 394 395 396
    _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) {
397
    assert(!debugNeedsLayout);
398 399 400 401 402 403 404 405
    _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) {
406
    assert(!debugNeedsLayout);
407 408 409 410 411 412 413 414 415 416 417 418 419 420
    _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) {
421
    assert(!debugNeedsLayout);
422 423 424 425
    _layoutTextWithConstraints(constraints);
    return _textPainter.getWordBoundary(position);
  }

426 427 428 429 430 431 432 433 434 435 436 437 438 439
  /// 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;
  }

440
  @override
441 442 443 444 445
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
    config
      ..label = text.toPlainText()
      ..textDirection = textDirection;
Hixie's avatar
Hixie committed
446
  }
447

448
  @override
449 450
  List<DiagnosticsNode> debugDescribeChildren() {
    return <DiagnosticsNode>[text.toDiagnosticsNode(name: 'text', style: DiagnosticsTreeStyle.transition)];
451
  }
Ian Hickson's avatar
Ian Hickson committed
452 453

  @override
454 455 456 457 458 459 460
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new EnumProperty<TextAlign>('textAlign', textAlign));
    properties.add(new EnumProperty<TextDirection>('textDirection', textDirection));
    properties.add(new FlagProperty('softWrap', value: softWrap, ifTrue: 'wrapping at box width', ifFalse: 'no wrapping except at line break characters', showName: true));
    properties.add(new EnumProperty<TextOverflow>('overflow', overflow));
    properties.add(new DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: 1.0));
461
    properties.add(new DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
462
    properties.add(new IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
Ian Hickson's avatar
Ian Hickson committed
463
  }
464
}