text_painter.dart 17.8 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 Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, TextBox;
6

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

11
import 'basic_types.dart';
12
import 'text_span.dart';
13

14 15
final String _kZeroWidthSpace = new String.fromCharCode(0x200B);

16
/// An object that paints a [TextSpan] tree into a [Canvas].
Hixie's avatar
Hixie committed
17 18 19 20 21 22
///
/// To use a [TextPainter], follow these steps:
///
/// 1. Create a [TextSpan] tree and pass it to the [TextPainter]
///    constructor.
///
23
/// 2. Call [layout] to prepare the paragraph.
Hixie's avatar
Hixie committed
24
///
25
/// 3. Call [paint] as often as desired to paint the paragraph.
Hixie's avatar
Hixie committed
26 27 28 29
///
/// If the width of the area into which the text is being painted
/// changes, return to step 2. If the text to be painted changes,
/// return to step 1.
30 31 32
///
/// The default text style is white. To change the color of the text,
/// pass a [TextStyle] object to the [TextSpan] in `text`.
Adam Barth's avatar
Adam Barth committed
33
class TextPainter {
34 35
  /// Creates a text painter that paints the given text.
  ///
Ian Hickson's avatar
Ian Hickson committed
36 37 38 39
  /// The `text` and `textDirection` arguments are optional but [text] and
  /// [textDirection] must be non-null before calling [layout].
  ///
  /// The [textAlign] property must not be null.
40 41
  ///
  /// The [maxLines] property, if non-null, must be greater than zero.
42 43
  TextPainter({
    TextSpan text,
Ian Hickson's avatar
Ian Hickson committed
44 45
    TextAlign textAlign: TextAlign.start,
    TextDirection textDirection,
46
    double textScaleFactor: 1.0,
47
    int maxLines,
48
    String ellipsis,
49
  }) : assert(text == null || text.debugAssertIsValid()),
Ian Hickson's avatar
Ian Hickson committed
50
       assert(textAlign != null),
51
       assert(textScaleFactor != null),
52
       assert(maxLines == null || maxLines > 0),
53 54
       _text = text,
       _textAlign = textAlign,
Ian Hickson's avatar
Ian Hickson committed
55
       _textDirection = textDirection,
56 57 58
       _textScaleFactor = textScaleFactor,
       _maxLines = maxLines,
       _ellipsis = ellipsis;
59

60
  ui.Paragraph _paragraph;
61
  bool _needsLayout = true;
62

Florian Loitsch's avatar
Florian Loitsch committed
63
  /// The (potentially styled) text to paint.
64 65
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
Ian Hickson's avatar
Ian Hickson committed
66 67
  ///
  /// This and [textDirection] must be non-null before you call [layout].
68
  TextSpan get text => _text;
69
  TextSpan _text;
70
  set text(TextSpan value) {
71
    assert(value == null || value.debugAssertIsValid());
72 73
    if (_text == value)
      return;
74 75
    if (_text?.style != value?.style)
      _layoutTemplate = null;
76
    _text = value;
77 78 79 80 81
    _paragraph = null;
    _needsLayout = true;
  }

  /// How the text should be aligned horizontally.
82 83
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
Ian Hickson's avatar
Ian Hickson committed
84 85
  ///
  /// The [textAlign] property must not be null. It defaults to [TextAlign.start].
86 87
  TextAlign get textAlign => _textAlign;
  TextAlign _textAlign;
88
  set textAlign(TextAlign value) {
Ian Hickson's avatar
Ian Hickson committed
89
    assert(value != null);
90 91 92 93 94
    if (_textAlign == value)
      return;
    _textAlign = value;
    _paragraph = null;
    _needsLayout = true;
95 96
  }

Ian Hickson's avatar
Ian Hickson committed
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
  /// The default directionality of the text.
  ///
  /// This controls how the [TextAlign.start], [TextAlign.end], and
  /// [TextAlign.justify] values of [textAlign] are resolved.
  ///
  /// 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]
  /// context, the English phrase will be on the right and the Hebrow phrase on
  /// its left.
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
  ///
  /// This and [text] must be non-null before you call [layout].
  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;
  set textDirection(TextDirection value) {
    if (_textDirection == value)
      return;
    _textDirection = value;
    _paragraph = null;
    _layoutTemplate = null; // Shouldn't really matter, but for strict correctness...
    _needsLayout = true;
  }

123 124 125 126
  /// 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.
127 128
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
129 130 131 132 133 134 135 136 137 138 139
  double get textScaleFactor => _textScaleFactor;
  double _textScaleFactor;
  set textScaleFactor(double value) {
    assert(value != null);
    if (_textScaleFactor == value)
      return;
    _textScaleFactor = value;
    _paragraph = null;
    _needsLayout = true;
  }

140
  /// The string used to ellipsize overflowing text. Setting this to a non-empty
141
  /// string will cause this string to be substituted for the remaining text
142 143 144 145 146 147 148
  /// if the text can not fit within the specified maximum width.
  ///
  /// Specifically, the ellipsis is applied to the last line before the line
  /// truncated by [maxLines], if [maxLines] is non-null and that line overflows
  /// the width constraint, or to the first line that is wider than the width
  /// constraint, if [maxLines] is null. The width constraint is the `maxWidth`
  /// passed to [layout].
149 150
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
151 152 153 154 155
  ///
  /// The higher layers of the system, such as the [Text] widget, represent
  /// overflow effects using the [TextOverflow] enum. The
  /// [TextOverflow.ellipsis] value corresponds to setting this property to
  /// U+2026 HORIZONTAL ELLIPSIS (…).
156 157 158 159 160 161 162 163 164 165 166
  String get ellipsis => _ellipsis;
  String _ellipsis;
  set ellipsis(String value) {
    assert(value == null || value.isNotEmpty);
    if (_ellipsis == value)
      return;
    _ellipsis = value;
    _paragraph = null;
    _needsLayout = true;
  }

167 168
  /// An optional maximum number of lines for the text to span, wrapping if
  /// necessary.
169
  ///
170 171
  /// If the text exceeds the given number of lines, it is truncated such that
  /// subsequent lines are dropped.
172 173
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
174 175
  int get maxLines => _maxLines;
  int _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 186
    if (_maxLines == value)
      return;
    _maxLines = value;
    _paragraph = null;
    _needsLayout = true;
  }

  ui.Paragraph _layoutTemplate;
187

Ian Hickson's avatar
Ian Hickson committed
188 189 190 191 192
  ui.ParagraphStyle _createParagraphStyle([TextDirection defaultTextDirection]) {
    // The defaultTextDirection argument is used for preferredLineHeight in case
    // textDirection hasn't yet been set.
    assert(textAlign != null);
    assert(textDirection != null || defaultTextDirection != null, 'TextPainter.textDirection must be set to a non-null value before using the TextPainter.');
193 194
    return _text.style?.getParagraphStyle(
      textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
195
      textDirection: textDirection ?? defaultTextDirection,
196 197 198 199 200
      textScaleFactor: textScaleFactor,
      maxLines: _maxLines,
      ellipsis: _ellipsis,
    ) ?? new ui.ParagraphStyle(
      textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
201
      textDirection: textDirection ?? defaultTextDirection,
202 203 204 205 206
      maxLines: maxLines,
      ellipsis: ellipsis,
    );
  }

207
  /// The height of a zero-width space in [text] in logical pixels.
208
  ///
209 210
  /// Not every line of text in [text] will have this height, but this height
  /// is "typical" for text in [text] and useful for sizing other objects
211
  /// relative a typical line of text.
212 213
  ///
  /// Obtaining this value does not require calling [layout].
Ian Hickson's avatar
Ian Hickson committed
214 215 216 217 218
  ///
  /// The style of the [text] property is used to determine the font settings
  /// that contribute to the [preferredLineHeight]. If [text] is null or if it
  /// specifies no styles, the default [TextStyle] values are used (a 10 pixel
  /// sans-serif font).
219 220
  double get preferredLineHeight {
    if (_layoutTemplate == null) {
Ian Hickson's avatar
Ian Hickson committed
221 222 223 224
      final ui.ParagraphBuilder builder = new ui.ParagraphBuilder(
        _createParagraphStyle(TextDirection.rtl),
      ); // direction doesn't matter, text is just a zero width space
      if (text?.style != null)
225 226 227 228 229 230 231
        builder.pushStyle(text.style.getTextStyle(textScaleFactor: textScaleFactor));
      builder.addText(_kZeroWidthSpace);
      _layoutTemplate = builder.build()
        ..layout(new ui.ParagraphConstraints(width: double.INFINITY));
    }
    return _layoutTemplate.height;
  }
232

233 234 235 236 237 238 239 240 241 242 243
  // Unfortunately, using full precision floating point here causes bad layouts
  // because floating point math isn't associative. If we add and subtract
  // padding, for example, we'll get different values when we estimate sizes and
  // when we actually compute layout because the operations will end up associated
  // differently. To work around this problem for now, we round fractional pixel
  // values up to the nearest whole pixel value. The right long-term fix is to do
  // layout using fixed precision arithmetic.
  double _applyFloatingPointHack(double layoutValue) {
    return layoutValue.ceilToDouble();
  }

244 245
  /// The width at which decreasing the width of the text would prevent it from
  /// painting itself completely within its bounds.
246
  ///
247
  /// Valid only after [layout] has been called.
248
  double get minIntrinsicWidth {
249 250 251 252
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.minIntrinsicWidth);
  }

Florian Loitsch's avatar
Florian Loitsch committed
253
  /// The width at which increasing the width of the text no longer decreases the height.
254
  ///
255
  /// Valid only after [layout] has been called.
256
  double get maxIntrinsicWidth {
257 258 259 260
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.maxIntrinsicWidth);
  }

261 262
  /// The horizontal space required to paint this text.
  ///
263
  /// Valid only after [layout] has been called.
264 265 266 267 268
  double get width {
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.width);
  }

269 270
  /// The vertical space required to paint this text.
  ///
271
  /// Valid only after [layout] has been called.
272 273 274 275 276
  double get height {
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.height);
  }

277 278
  /// The amount of space required to paint this text.
  ///
279
  /// Valid only after [layout] has been called.
280
  Size get size {
281
    assert(!_needsLayout);
282
    return new Size(width, height);
283 284
  }

285 286
  /// Returns the distance from the top of the text to the first baseline of the
  /// given type.
287
  ///
288
  /// Valid only after [layout] has been called.
289 290 291 292 293 294 295 296
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    assert(!_needsLayout);
    switch (baseline) {
      case TextBaseline.alphabetic:
        return _paragraph.alphabeticBaseline;
      case TextBaseline.ideographic:
        return _paragraph.ideographicBaseline;
    }
pq's avatar
pq committed
297
    assert(baseline != null);
pq's avatar
pq committed
298
    return null;
299 300
  }

301 302 303 304 305
  /// Whether any text was truncated or ellipsized.
  ///
  /// If [maxLines] is not null, this is true if there were more lines to be
  /// drawn than the given [maxLines], and thus at least one line was omitted in
  /// the output; otherwise it is false.
306
  ///
307 308 309 310 311
  /// If [maxLines] is null, this is true if [ellipsis] is not the empty string
  /// and there was a line that overflowed the `maxWidth` argument passed to
  /// [layout]; otherwise it is false.
  ///
  /// Valid only after [layout] has been called.
312 313 314 315 316
  bool get didExceedMaxLines {
    assert(!_needsLayout);
    return _paragraph.didExceedMaxLines;
  }

317 318
  double _lastMinWidth;
  double _lastMaxWidth;
319

Florian Loitsch's avatar
Florian Loitsch committed
320
  /// Computes the visual position of the glyphs for painting the text.
321
  ///
322
  /// The text will layout with a width that's as close to its max intrinsic
Ian Hickson's avatar
Ian Hickson committed
323 324 325 326 327
  /// width as possible while still being greater than or equal to `minWidth` and
  /// less than or equal to `maxWidth`.
  ///
  /// The [text] and [textDirection] properties must be non-null before this is
  /// called.
328
  void layout({ double minWidth: 0.0, double maxWidth: double.INFINITY }) {
Ian Hickson's avatar
Ian Hickson committed
329 330
    assert(text != null, 'TextPainter.text must be set to a non-null value before using the TextPainter.');
    assert(textDirection != null, 'TextPainter.textDirection must be set to a non-null value before using the TextPainter.');
331
    if (!_needsLayout && minWidth == _lastMinWidth && maxWidth == _lastMaxWidth)
332 333
      return;
    _needsLayout = false;
334
    if (_paragraph == null) {
335
      final ui.ParagraphBuilder builder = new ui.ParagraphBuilder(_createParagraphStyle());
336 337
      _text.build(builder, textScaleFactor: textScaleFactor);
      _paragraph = builder.build();
338
    }
339 340 341 342 343 344 345 346
    _lastMinWidth = minWidth;
    _lastMaxWidth = maxWidth;
    _paragraph.layout(new ui.ParagraphConstraints(width: maxWidth));
    if (minWidth != maxWidth) {
      final double newWidth = maxIntrinsicWidth.clamp(minWidth, maxWidth);
      if (newWidth != width)
        _paragraph.layout(new ui.ParagraphConstraints(width: newWidth));
    }
347 348
  }

Florian Loitsch's avatar
Florian Loitsch committed
349
  /// Paints the text onto the given canvas at the given offset.
350
  ///
351
  /// Valid only after [layout] has been called.
352 353 354 355 356 357 358 359 360
  ///
  /// If you cannot see the text being painted, check that your text color does
  /// not conflict with the background on which you are drawing. The default
  /// text color is white (to contrast with the default black background color),
  /// so if you are writing an application with a white background, the text
  /// will not be visible by default.
  ///
  /// To set the text style, specify a [TextStyle] when creating the [TextSpan]
  /// that you pass to the [TextPainter] constructor or to the [text] property.
361
  void paint(Canvas canvas, Offset offset) {
362 363 364 365 366 367 368 369
    assert(() {
      if (_needsLayout) {
        throw new FlutterError(
          'TextPainter.paint called when text geometry was not yet calculated.\n'
          'Please call layout() before paint() to position the text before painting it.'
        );
      }
      return true;
370
    }());
Adam Barth's avatar
Adam Barth committed
371
    canvas.drawParagraph(_paragraph, offset);
372
  }
373

374 375 376 377
  bool _isUtf16Surrogate(int value) {
    return value & 0xF800 == 0xD800;
  }

378
  Offset _getOffsetFromUpstream(int offset, Rect caretPrototype) {
379
    final int prevCodeUnit = _text.codeUnitAt(offset - 1);
380 381
    if (prevCodeUnit == null)
      return null;
382 383
    final int prevRuneOffset = _isUtf16Surrogate(prevCodeUnit) ? offset - 2 : offset - 1;
    final List<ui.TextBox> boxes = _paragraph.getBoxesForRange(prevRuneOffset, offset);
384 385
    if (boxes.isEmpty)
      return null;
386 387 388
    final ui.TextBox box = boxes[0];
    final double caretEnd = box.end;
    final double dx = box.direction == TextDirection.rtl ? caretEnd : caretEnd - caretPrototype.width;
389
    return new Offset(dx, box.top);
390 391 392
  }

  Offset _getOffsetFromDownstream(int offset, Rect caretPrototype) {
393
    final int nextCodeUnit = _text.codeUnitAt(offset + 1);
394 395
    if (nextCodeUnit == null)
      return null;
396 397
    final int nextRuneOffset = _isUtf16Surrogate(nextCodeUnit) ? offset + 2 : offset + 1;
    final List<ui.TextBox> boxes = _paragraph.getBoxesForRange(offset, nextRuneOffset);
398 399
    if (boxes.isEmpty)
      return null;
400 401 402
    final ui.TextBox box = boxes[0];
    final double caretStart = box.start;
    final double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
403
    return new Offset(dx, box.top);
404 405
  }

406
  Offset get _emptyOffset {
Ian Hickson's avatar
Ian Hickson committed
407 408 409
    assert(!_needsLayout); // implies textDirection is non-null
    assert(textAlign != null);
    switch (textAlign) {
410 411 412 413 414 415
      case TextAlign.left:
        return Offset.zero;
      case TextAlign.right:
        return new Offset(width, 0.0);
      case TextAlign.center:
        return new Offset(width / 2.0, 0.0);
Ian Hickson's avatar
Ian Hickson committed
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434
      case TextAlign.justify:
      case TextAlign.start:
        assert(textDirection != null);
        switch (textDirection) {
          case TextDirection.rtl:
            return new Offset(width, 0.0);
          case TextDirection.ltr:
            return Offset.zero;
        }
        return null;
      case TextAlign.end:
        assert(textDirection != null);
        switch (textDirection) {
          case TextDirection.rtl:
            return Offset.zero;
          case TextDirection.ltr:
            return new Offset(width, 0.0);
        }
        return null;
435 436 437 438
    }
    return null;
  }

439
  /// Returns the offset at which to paint the caret.
440
  ///
441
  /// Valid only after [layout] has been called.
442 443
  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
    assert(!_needsLayout);
444
    final int offset = position.offset;
445 446 447 448
    switch (position.affinity) {
      case TextAffinity.upstream:
        return _getOffsetFromUpstream(offset, caretPrototype)
            ?? _getOffsetFromDownstream(offset, caretPrototype)
449
            ?? _emptyOffset;
450 451 452
      case TextAffinity.downstream:
        return _getOffsetFromDownstream(offset, caretPrototype)
            ?? _getOffsetFromUpstream(offset, caretPrototype)
453
            ?? _emptyOffset;
454
    }
pq's avatar
pq committed
455
    assert(position.affinity != null);
pq's avatar
pq committed
456
    return null;
457 458 459 460 461 462 463 464 465 466 467 468
  }

  /// 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.
  List<ui.TextBox> getBoxesForSelection(TextSelection selection) {
    assert(!_needsLayout);
    return _paragraph.getBoxesForRange(selection.start, selection.end);
  }

469
  /// Returns the position within the text for the given pixel offset.
470 471 472 473 474
  TextPosition getPositionForOffset(Offset offset) {
    assert(!_needsLayout);
    return _paragraph.getPositionForOffset(offset);
  }

475 476 477 478 479 480 481
  /// 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>.
482 483
  TextRange getWordBoundary(TextPosition position) {
    assert(!_needsLayout);
484
    final List<int> indices = _paragraph.getWordBoundary(position.offset);
485 486
    return new TextRange(start: indices[0], end: indices[1]);
  }
487
}