text_painter.dart 13.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 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 36
  /// Creates a text painter that paints the given text.
  ///
  /// The text argument is optional but [text] must be non-null before calling
37
  /// [layout].
38 39
  TextPainter({
    TextSpan text,
40
    TextAlign textAlign,
41
    double textScaleFactor: 1.0,
42
    int maxLines,
43
    String ellipsis,
44
  }) : _text = text, _textAlign = textAlign, _textScaleFactor = textScaleFactor, _maxLines = maxLines, _ellipsis = ellipsis {
45
    assert(text == null || text.debugAssertIsValid());
46
    assert(textScaleFactor != null);
47 48
  }

49
  ui.Paragraph _paragraph;
50
  bool _needsLayout = true;
51

Florian Loitsch's avatar
Florian Loitsch committed
52
  /// The (potentially styled) text to paint.
53 54
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
55
  TextSpan get text => _text;
56
  TextSpan _text;
57
  set text(TextSpan value) {
58
    assert(value == null || value.debugAssertIsValid());
59 60
    if (_text == value)
      return;
61 62
    if (_text?.style != value?.style)
      _layoutTemplate = null;
63
    _text = value;
64 65 66 67 68
    _paragraph = null;
    _needsLayout = true;
  }

  /// How the text should be aligned horizontally.
69 70
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
71 72
  TextAlign get textAlign => _textAlign;
  TextAlign _textAlign;
73
  set textAlign(TextAlign value) {
74 75 76 77 78
    if (_textAlign == value)
      return;
    _textAlign = value;
    _paragraph = null;
    _needsLayout = true;
79 80
  }

81 82 83 84
  /// 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.
85 86
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
87 88 89 90 91 92 93 94 95 96 97
  double get textScaleFactor => _textScaleFactor;
  double _textScaleFactor;
  set textScaleFactor(double value) {
    assert(value != null);
    if (_textScaleFactor == value)
      return;
    _textScaleFactor = value;
    _paragraph = null;
    _needsLayout = true;
  }

98 99 100
  /// The string used to ellipsize overflowing text.  Setting this to a nonempty
  /// string will cause this string to be substituted for the remaining text
  /// if the text can not fit within the specificed maximum width.
101 102
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
103 104 105 106 107 108 109 110 111 112 113
  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;
  }

114
  /// An optional maximum number of lines for the text to span, wrapping if necessary.
115
  ///
116 117
  /// If the text exceeds the given number of lines, it will be truncated according
  /// to [overflow].
118 119
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
120 121 122 123 124 125 126 127 128 129 130
  int get maxLines => _maxLines;
  int _maxLines;
  set maxLines(int value) {
    if (_maxLines == value)
      return;
    _maxLines = value;
    _paragraph = null;
    _needsLayout = true;
  }

  ui.Paragraph _layoutTemplate;
131 132 133 134 135 136

  /// The height of a zero-width space in [style] in logical pixels.
  ///
  /// Not every line of text in [style] will have this height, but this height
  /// is "typical" for text in [style] and useful for sizing other objects
  /// relative a typical line of text.
137 138 139
  double get preferredLineHeight {
    assert(text != null);
    if (_layoutTemplate == null) {
140
      final ui.ParagraphBuilder builder = new ui.ParagraphBuilder(new ui.ParagraphStyle());
141 142 143 144 145 146 147 148
      if (text.style != null)
        builder.pushStyle(text.style.getTextStyle(textScaleFactor: textScaleFactor));
      builder.addText(_kZeroWidthSpace);
      _layoutTemplate = builder.build()
        ..layout(new ui.ParagraphConstraints(width: double.INFINITY));
    }
    return _layoutTemplate.height;
  }
149

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

Florian Loitsch's avatar
Florian Loitsch committed
161
  /// The width at which decreasing the width of the text would prevent it from painting itself completely within its bounds.
162
  ///
163
  /// Valid only after [layout] has been called.
164
  double get minIntrinsicWidth {
165 166 167 168
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.minIntrinsicWidth);
  }

Florian Loitsch's avatar
Florian Loitsch committed
169
  /// The width at which increasing the width of the text no longer decreases the height.
170
  ///
171
  /// Valid only after [layout] has been called.
172
  double get maxIntrinsicWidth {
173 174 175 176
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.maxIntrinsicWidth);
  }

177 178
  /// The horizontal space required to paint this text.
  ///
179
  /// Valid only after [layout] has been called.
180 181 182 183 184
  double get width {
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.width);
  }

185 186
  /// The vertical space required to paint this text.
  ///
187
  /// Valid only after [layout] has been called.
188 189 190 191 192
  double get height {
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.height);
  }

193 194
  /// The amount of space required to paint this text.
  ///
195
  /// Valid only after [layout] has been called.
196
  Size get size {
197
    assert(!_needsLayout);
198
    return new Size(width, height);
199 200
  }

Florian Loitsch's avatar
Florian Loitsch committed
201
  /// Returns the distance from the top of the text to the first baseline of the given type.
202
  ///
203
  /// Valid only after [layout] has been called.
204 205 206 207 208 209 210 211
  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
212
    assert(baseline != null);
pq's avatar
pq committed
213
    return null;
214 215
  }

216 217 218 219
  /// Whether the previous call to [layout] attempted to produce more than
  /// [maxLines] lines.
  ///
  /// If [didExceedMaxLines] is true, then any overflow effect is operative.
220 221 222 223 224
  bool get didExceedMaxLines {
    assert(!_needsLayout);
    return _paragraph.didExceedMaxLines;
  }

225 226
  double _lastMinWidth;
  double _lastMaxWidth;
227

Florian Loitsch's avatar
Florian Loitsch committed
228
  /// Computes the visual position of the glyphs for painting the text.
229
  ///
230 231 232 233
  /// The text will layout with a width that's as close to its max intrinsic
  /// width as possible while still being greater than or equal to minWidth and
  /// less than or equal to maxWidth.
  void layout({ double minWidth: 0.0, double maxWidth: double.INFINITY }) {
234
    assert(_text != null);
235
    if (!_needsLayout && minWidth == _lastMinWidth && maxWidth == _lastMaxWidth)
236 237
      return;
    _needsLayout = false;
238
    if (_paragraph == null) {
239 240 241
      ui.ParagraphStyle paragraphStyle = _text.style?.getParagraphStyle(
        textAlign: textAlign,
        textScaleFactor: textScaleFactor,
242
        maxLines: _maxLines,
243 244
        ellipsis: _ellipsis,
      );
245 246 247 248 249
      paragraphStyle ??= new ui.ParagraphStyle(
        textAlign: textAlign,
        maxLines: maxLines,
        ellipsis: ellipsis,
      );
250
      final ui.ParagraphBuilder builder = new ui.ParagraphBuilder(paragraphStyle);
251 252
      _text.build(builder, textScaleFactor: textScaleFactor);
      _paragraph = builder.build();
253
    }
254 255 256 257 258 259 260 261
    _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));
    }
262 263
  }

Florian Loitsch's avatar
Florian Loitsch committed
264
  /// Paints the text onto the given canvas at the given offset.
265
  ///
266
  /// Valid only after [layout] has been called.
267 268 269 270 271 272 273 274 275
  ///
  /// 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.
276
  void paint(Canvas canvas, Offset offset) {
277 278 279 280 281 282 283 284 285
    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;
    });
Adam Barth's avatar
Adam Barth committed
286
    canvas.drawParagraph(_paragraph, offset);
287
  }
288

289 290 291 292
  bool _isUtf16Surrogate(int value) {
    return value & 0xF800 == 0xD800;
  }

293
  Offset _getOffsetFromUpstream(int offset, Rect caretPrototype) {
294
    final int prevCodeUnit = _text.codeUnitAt(offset - 1);
295 296
    if (prevCodeUnit == null)
      return null;
297 298
    final int prevRuneOffset = _isUtf16Surrogate(prevCodeUnit) ? offset - 2 : offset - 1;
    final List<ui.TextBox> boxes = _paragraph.getBoxesForRange(prevRuneOffset, offset);
299 300
    if (boxes.isEmpty)
      return null;
301 302 303
    final ui.TextBox box = boxes[0];
    final double caretEnd = box.end;
    final double dx = box.direction == TextDirection.rtl ? caretEnd : caretEnd - caretPrototype.width;
304
    return new Offset(dx, box.top);
305 306 307
  }

  Offset _getOffsetFromDownstream(int offset, Rect caretPrototype) {
308
    final int nextCodeUnit = _text.codeUnitAt(offset + 1);
309 310
    if (nextCodeUnit == null)
      return null;
311 312
    final int nextRuneOffset = _isUtf16Surrogate(nextCodeUnit) ? offset + 2 : offset + 1;
    final List<ui.TextBox> boxes = _paragraph.getBoxesForRange(offset, nextRuneOffset);
313 314
    if (boxes.isEmpty)
      return null;
315 316 317
    final ui.TextBox box = boxes[0];
    final double caretStart = box.start;
    final double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
318
    return new Offset(dx, box.top);
319 320
  }

321 322 323 324 325 326 327 328 329 330 331 332 333 334
  Offset get _emptyOffset {
    // TODO(abarth): Handle the directionality of the text painter itself.
    switch (textAlign ?? TextAlign.left) {
      case TextAlign.left:
      case TextAlign.justify:
        return Offset.zero;
      case TextAlign.right:
        return new Offset(width, 0.0);
      case TextAlign.center:
        return new Offset(width / 2.0, 0.0);
    }
    return null;
  }

335
  /// Returns the offset at which to paint the caret.
336
  ///
337
  /// Valid only after [layout] has been called.
338 339
  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
    assert(!_needsLayout);
340
    final int offset = position.offset;
341 342 343 344
    switch (position.affinity) {
      case TextAffinity.upstream:
        return _getOffsetFromUpstream(offset, caretPrototype)
            ?? _getOffsetFromDownstream(offset, caretPrototype)
345
            ?? _emptyOffset;
346 347 348
      case TextAffinity.downstream:
        return _getOffsetFromDownstream(offset, caretPrototype)
            ?? _getOffsetFromUpstream(offset, caretPrototype)
349
            ?? _emptyOffset;
350
    }
pq's avatar
pq committed
351
    assert(position.affinity != null);
pq's avatar
pq committed
352
    return null;
353 354 355 356 357 358 359 360 361 362 363 364
  }

  /// 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);
  }

365
  /// Returns the position within the text for the given pixel offset.
366 367 368 369 370
  TextPosition getPositionForOffset(Offset offset) {
    assert(!_needsLayout);
    return _paragraph.getPositionForOffset(offset);
  }

371 372 373 374 375 376 377
  /// 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>.
378 379
  TextRange getWordBoundary(TextPosition position) {
    assert(!_needsLayout);
380
    final List<int> indices = _paragraph.getWordBoundary(position.offset);
381 382
    return new TextRange(start: indices[0], end: indices[1]);
  }
383
}