text_painter.dart 20 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, Locale;
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

Ian Hickson's avatar
Ian Hickson committed
14 15
export 'package:flutter/services.dart' show TextRange, TextSelection;

16 17
final String _kZeroWidthSpace = new String.fromCharCode(0x200B);

18
/// An object that paints a [TextSpan] tree into a [Canvas].
Hixie's avatar
Hixie committed
19 20 21 22 23 24
///
/// To use a [TextPainter], follow these steps:
///
/// 1. Create a [TextSpan] tree and pass it to the [TextPainter]
///    constructor.
///
25
/// 2. Call [layout] to prepare the paragraph.
Hixie's avatar
Hixie committed
26
///
27
/// 3. Call [paint] as often as desired to paint the paragraph.
Hixie's avatar
Hixie committed
28 29 30 31
///
/// 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.
32 33 34
///
/// 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
35
class TextPainter {
36 37
  /// Creates a text painter that paints the given text.
  ///
Ian Hickson's avatar
Ian Hickson committed
38 39 40 41
  /// 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.
42 43
  ///
  /// The [maxLines] property, if non-null, must be greater than zero.
44 45
  TextPainter({
    TextSpan text,
Ian Hickson's avatar
Ian Hickson committed
46 47
    TextAlign textAlign: TextAlign.start,
    TextDirection textDirection,
48
    double textScaleFactor: 1.0,
49
    int maxLines,
50
    String ellipsis,
51
    ui.Locale locale,
52
  }) : assert(text == null || text.debugAssertIsValid()),
Ian Hickson's avatar
Ian Hickson committed
53
       assert(textAlign != null),
54
       assert(textScaleFactor != null),
55
       assert(maxLines == null || maxLines > 0),
56 57
       _text = text,
       _textAlign = textAlign,
Ian Hickson's avatar
Ian Hickson committed
58
       _textDirection = textDirection,
59 60
       _textScaleFactor = textScaleFactor,
       _maxLines = maxLines,
61 62
       _ellipsis = ellipsis,
       _locale = locale;
63

64
  ui.Paragraph _paragraph;
65
  bool _needsLayout = true;
66

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

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

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

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

145
  /// The string used to ellipsize overflowing text. Setting this to a non-empty
146
  /// string will cause this string to be substituted for the remaining text
147 148 149 150 151 152 153
  /// 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].
154 155
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
156 157 158 159 160
  ///
  /// 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 (…).
161 162 163 164 165 166 167 168 169 170 171
  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;
  }

172 173 174 175 176 177 178 179 180 181 182
  /// The locale used to select region-specific glyphs.
  ui.Locale get locale => _locale;
  ui.Locale _locale;
  set locale(ui.Locale value) {
    if (_locale == value)
      return;
    _locale = value;
    _paragraph = null;
    _needsLayout = true;
  }

183 184
  /// An optional maximum number of lines for the text to span, wrapping if
  /// necessary.
185
  ///
186 187
  /// If the text exceeds the given number of lines, it is truncated such that
  /// subsequent lines are dropped.
188 189
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
190 191
  int get maxLines => _maxLines;
  int _maxLines;
192
  /// The value may be null. If it is not null, then it must be greater than zero.
193
  set maxLines(int value) {
194
    assert(value == null || value > 0);
195 196 197 198 199 200 201 202
    if (_maxLines == value)
      return;
    _maxLines = value;
    _paragraph = null;
    _needsLayout = true;
  }

  ui.Paragraph _layoutTemplate;
203

Ian Hickson's avatar
Ian Hickson committed
204 205 206 207 208
  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.');
209 210
    return _text.style?.getParagraphStyle(
      textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
211
      textDirection: textDirection ?? defaultTextDirection,
212 213 214
      textScaleFactor: textScaleFactor,
      maxLines: _maxLines,
      ellipsis: _ellipsis,
215
      locale: _locale,
216 217
    ) ?? new ui.ParagraphStyle(
      textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
218
      textDirection: textDirection ?? defaultTextDirection,
219 220
      maxLines: maxLines,
      ellipsis: ellipsis,
221
      locale: locale,
222 223 224
    );
  }

225
  /// The height of a zero-width space in [text] in logical pixels.
226
  ///
227 228
  /// 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
229
  /// relative a typical line of text.
230 231
  ///
  /// Obtaining this value does not require calling [layout].
Ian Hickson's avatar
Ian Hickson committed
232 233 234 235 236
  ///
  /// 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).
237 238
  double get preferredLineHeight {
    if (_layoutTemplate == null) {
Ian Hickson's avatar
Ian Hickson committed
239 240 241 242
      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)
243 244 245
        builder.pushStyle(text.style.getTextStyle(textScaleFactor: textScaleFactor));
      builder.addText(_kZeroWidthSpace);
      _layoutTemplate = builder.build()
246
        ..layout(new ui.ParagraphConstraints(width: double.infinity));
247 248 249
    }
    return _layoutTemplate.height;
  }
250

251 252 253 254 255 256 257 258 259 260 261
  // 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();
  }

262 263
  /// The width at which decreasing the width of the text would prevent it from
  /// painting itself completely within its bounds.
264
  ///
265
  /// Valid only after [layout] has been called.
266
  double get minIntrinsicWidth {
267 268 269 270
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.minIntrinsicWidth);
  }

Florian Loitsch's avatar
Florian Loitsch committed
271
  /// The width at which increasing the width of the text no longer decreases the height.
272
  ///
273
  /// Valid only after [layout] has been called.
274
  double get maxIntrinsicWidth {
275 276 277 278
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.maxIntrinsicWidth);
  }

279 280
  /// The horizontal space required to paint this text.
  ///
281
  /// Valid only after [layout] has been called.
282 283 284 285 286
  double get width {
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.width);
  }

287 288
  /// The vertical space required to paint this text.
  ///
289
  /// Valid only after [layout] has been called.
290 291 292 293 294
  double get height {
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.height);
  }

295 296
  /// The amount of space required to paint this text.
  ///
297
  /// Valid only after [layout] has been called.
298
  Size get size {
299
    assert(!_needsLayout);
300
    return new Size(width, height);
301 302
  }

303 304 305 306 307 308 309 310 311 312 313 314
  // Workaround for https://github.com/flutter/flutter/issues/13303
  double _workaroundBaselineBug(double value, TextBaseline baseline) {
    if (value >= 0.0)
      return value;

    final ui.ParagraphBuilder builder = new ui.ParagraphBuilder(
      _createParagraphStyle(TextDirection.ltr),
    );
    if (text?.style != null)
      builder.pushStyle(text.style.getTextStyle(textScaleFactor: textScaleFactor));
    builder.addText(_kZeroWidthSpace);
    final ui.Paragraph paragraph = builder.build()
315
      ..layout(new ui.ParagraphConstraints(width: double.infinity));
316 317 318 319 320 321 322 323 324 325

    switch (baseline) {
      case TextBaseline.alphabetic:
        return paragraph.alphabeticBaseline;
      case TextBaseline.ideographic:
       return paragraph.ideographicBaseline;
    }
    return null;
  }

326 327
  /// Returns the distance from the top of the text to the first baseline of the
  /// given type.
328
  ///
329
  /// Valid only after [layout] has been called.
330 331
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    assert(!_needsLayout);
Ian Hickson's avatar
Ian Hickson committed
332
    assert(baseline != null);
333 334
    switch (baseline) {
      case TextBaseline.alphabetic:
335
        return _workaroundBaselineBug(_paragraph.alphabeticBaseline, baseline);
336
      case TextBaseline.ideographic:
337
       return _workaroundBaselineBug(_paragraph.ideographicBaseline, baseline);
338
    }
pq's avatar
pq committed
339
    return null;
340 341
  }

342 343 344 345 346
  /// 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.
347
  ///
348 349 350 351 352
  /// 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.
353 354 355 356 357
  bool get didExceedMaxLines {
    assert(!_needsLayout);
    return _paragraph.didExceedMaxLines;
  }

358 359
  double _lastMinWidth;
  double _lastMaxWidth;
360

Florian Loitsch's avatar
Florian Loitsch committed
361
  /// Computes the visual position of the glyphs for painting the text.
362
  ///
363
  /// The text will layout with a width that's as close to its max intrinsic
Ian Hickson's avatar
Ian Hickson committed
364 365 366 367 368
  /// 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.
369
  void layout({ double minWidth: 0.0, double maxWidth: double.infinity }) {
Ian Hickson's avatar
Ian Hickson committed
370 371
    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.');
372
    if (!_needsLayout && minWidth == _lastMinWidth && maxWidth == _lastMaxWidth)
373 374
      return;
    _needsLayout = false;
375
    if (_paragraph == null) {
376
      final ui.ParagraphBuilder builder = new ui.ParagraphBuilder(_createParagraphStyle());
377 378
      _text.build(builder, textScaleFactor: textScaleFactor);
      _paragraph = builder.build();
379
    }
380 381 382 383 384 385 386 387
    _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));
    }
388 389
  }

Florian Loitsch's avatar
Florian Loitsch committed
390
  /// Paints the text onto the given canvas at the given offset.
391
  ///
392
  /// Valid only after [layout] has been called.
393 394 395 396 397 398 399 400 401
  ///
  /// 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.
402
  void paint(Canvas canvas, Offset offset) {
403 404 405 406 407 408 409 410
    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;
411
    }());
Adam Barth's avatar
Adam Barth committed
412
    canvas.drawParagraph(_paragraph, offset);
413
  }
414

415 416 417 418
  bool _isUtf16Surrogate(int value) {
    return value & 0xF800 == 0xD800;
  }

419 420 421 422 423 424
  /// Returns the closest offset after `offset` at which the inout cursor can be
  /// positioned.
  int getOffsetAfter(int offset) {
    final int nextCodeUnit = _text.codeUnitAt(offset);
    if (nextCodeUnit == null)
      return null;
425
    // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
426 427 428 429 430 431 432 433 434
    return _isUtf16Surrogate(nextCodeUnit) ? offset + 2 : offset + 1;
  }

  /// Returns the closest offset before `offset` at which the inout cursor can
  /// be positioned.
  int getOffsetBefore(int offset) {
    final int prevCodeUnit = _text.codeUnitAt(offset - 1);
    if (prevCodeUnit == null)
      return null;
435
    // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
436 437 438
    return _isUtf16Surrogate(prevCodeUnit) ? offset - 2 : offset - 1;
  }

439
  Offset _getOffsetFromUpstream(int offset, Rect caretPrototype) {
440
    final int prevCodeUnit = _text.codeUnitAt(offset - 1);
441 442
    if (prevCodeUnit == null)
      return null;
443
    final int prevRuneOffset = _isUtf16Surrogate(prevCodeUnit) ? offset - 2 : offset - 1;
Ian Hickson's avatar
Ian Hickson committed
444
    final List<TextBox> boxes = _paragraph.getBoxesForRange(prevRuneOffset, offset);
445 446
    if (boxes.isEmpty)
      return null;
Ian Hickson's avatar
Ian Hickson committed
447
    final TextBox box = boxes[0];
448 449
    final double caretEnd = box.end;
    final double dx = box.direction == TextDirection.rtl ? caretEnd : caretEnd - caretPrototype.width;
450
    return new Offset(dx, box.top);
451 452 453
  }

  Offset _getOffsetFromDownstream(int offset, Rect caretPrototype) {
454
    final int nextCodeUnit = _text.codeUnitAt(offset);
455 456
    if (nextCodeUnit == null)
      return null;
457
    final int nextRuneOffset = _isUtf16Surrogate(nextCodeUnit) ? offset + 2 : offset + 1;
Ian Hickson's avatar
Ian Hickson committed
458
    final List<TextBox> boxes = _paragraph.getBoxesForRange(offset, nextRuneOffset);
459 460
    if (boxes.isEmpty)
      return null;
Ian Hickson's avatar
Ian Hickson committed
461
    final TextBox box = boxes[0];
462 463
    final double caretStart = box.start;
    final double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
464
    return new Offset(dx, box.top);
465 466
  }

467
  Offset get _emptyOffset {
Ian Hickson's avatar
Ian Hickson committed
468 469 470
    assert(!_needsLayout); // implies textDirection is non-null
    assert(textAlign != null);
    switch (textAlign) {
471 472 473 474 475 476
      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
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495
      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;
496 497 498 499
    }
    return null;
  }

500
  /// Returns the offset at which to paint the caret.
501
  ///
502
  /// Valid only after [layout] has been called.
503 504
  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
    assert(!_needsLayout);
505
    final int offset = position.offset;
Ian Hickson's avatar
Ian Hickson committed
506
    assert(position.affinity != null);
507 508 509 510
    switch (position.affinity) {
      case TextAffinity.upstream:
        return _getOffsetFromUpstream(offset, caretPrototype)
            ?? _getOffsetFromDownstream(offset, caretPrototype)
511
            ?? _emptyOffset;
512 513 514
      case TextAffinity.downstream:
        return _getOffsetFromDownstream(offset, caretPrototype)
            ?? _getOffsetFromUpstream(offset, caretPrototype)
515
            ?? _emptyOffset;
516
    }
pq's avatar
pq committed
517
    return null;
518 519 520 521 522 523 524
  }

  /// 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.
Ian Hickson's avatar
Ian Hickson committed
525
  List<TextBox> getBoxesForSelection(TextSelection selection) {
526 527 528 529
    assert(!_needsLayout);
    return _paragraph.getBoxesForRange(selection.start, selection.end);
  }

530
  /// Returns the position within the text for the given pixel offset.
531 532 533 534 535
  TextPosition getPositionForOffset(Offset offset) {
    assert(!_needsLayout);
    return _paragraph.getPositionForOffset(offset);
  }

536 537 538 539 540 541 542
  /// 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>.
543 544
  TextRange getWordBoundary(TextPosition position) {
    assert(!_needsLayout);
545
    final List<int> indices = _paragraph.getWordBoundary(position.offset);
546 547
    return new TextRange(start: indices[0], end: indices[1]);
  }
548
}