text_painter.dart 34.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:math' show min, max;
6
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, PlaceholderAlignment, LineMetrics, TextHeightBehavior, BoxHeightStyle, BoxWidthStyle;
7

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

11
import 'basic_types.dart';
12 13
import 'inline_span.dart';
import 'placeholder_span.dart';
14
import 'strut_style.dart';
15
import 'text_span.dart';
16

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

19 20 21 22 23 24 25 26 27 28 29 30 31
/// Holds the [Size] and baseline required to represent the dimensions of
/// a placeholder in text.
///
/// Placeholders specify an empty space in the text layout, which is used
/// to later render arbitrary inline widgets into defined by a [WidgetSpan].
///
/// The [size] and [alignment] properties are required and cannot be null.
///
/// See also:
///
///  * [WidgetSpan], a subclass of [InlineSpan] and [PlaceholderSpan] that
///    represents an inline widget embedded within text. The space this
///    widget takes is indicated by a placeholder.
32
///  * [RichText], a text widget that supports text inline widgets.
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
@immutable
class PlaceholderDimensions {
  /// Constructs a [PlaceholderDimensions] with the specified parameters.
  ///
  /// The `size` and `alignment` are required as a placeholder's dimensions
  /// require at least `size` and `alignment` to be fully defined.
  const PlaceholderDimensions({
    @required this.size,
    @required this.alignment,
    this.baseline,
    this.baselineOffset,
  }) : assert(size != null),
       assert(alignment != null);

  /// Width and height dimensions of the placeholder.
  final Size size;

  /// How to align the placeholder with the text.
  ///
  /// See also:
  ///
  ///  * [baseline], the baseline to align to when using
  ///    [ui.PlaceholderAlignment.baseline],
  ///    [ui.PlaceholderAlignment.aboveBaseline],
  ///    or [ui.PlaceholderAlignment.underBaseline].
  ///  * [baselineOffset], the distance of the alphabetic baseline from the upper
  ///    edge of the placeholder.
  final ui.PlaceholderAlignment alignment;

  /// Distance of the [baseline] from the upper edge of the placeholder.
  ///
  /// Only used when [alignment] is [ui.PlaceholderAlignment.baseline].
  final double baselineOffset;

  /// The [TextBaseline] to align to. Used with:
  ///
  ///  * [ui.PlaceholderAlignment.baseline]
  ///  * [ui.PlaceholderAlignment.aboveBaseline]
  ///  * [ui.PlaceholderAlignment.underBaseline]
  ///  * [ui.PlaceholderAlignment.middle]
  final TextBaseline baseline;

  @override
  String toString() {
    return 'PlaceholderDimensions($size, $baseline)';
  }
}

81
/// The different ways of measuring the width of one or more lines of text.
82
///
83
/// See [Text.textWidthBasis], for example.
84
enum TextWidthBasis {
85
  /// multiline text will take up the full width given by the parent. For single
86 87 88 89 90 91 92 93 94 95
  /// line text, only the minimum amount of width needed to contain the text
  /// will be used. A common use case for this is a standard series of
  /// paragraphs.
  parent,

  /// The width will be exactly enough to contain the longest line and no
  /// longer. A common use case for this is chat bubbles.
  longestLine,
}

96 97 98
/// This is used to cache and pass the computed metrics regarding the
/// caret's size and position. This is preferred due to the expensive
/// nature of the calculation.
99 100 101 102 103 104 105 106 107 108
class _CaretMetrics {
  const _CaretMetrics({this.offset, this.fullHeight});
  /// The offset of the top left corner of the caret from the top left
  /// corner of the paragraph.
  final Offset offset;

  /// The full height of the glyph at the caret position.
  final double fullHeight;
}

109
/// An object that paints a [TextSpan] tree into a [Canvas].
Hixie's avatar
Hixie committed
110 111 112 113 114 115
///
/// To use a [TextPainter], follow these steps:
///
/// 1. Create a [TextSpan] tree and pass it to the [TextPainter]
///    constructor.
///
116
/// 2. Call [layout] to prepare the paragraph.
Hixie's avatar
Hixie committed
117
///
118
/// 3. Call [paint] as often as desired to paint the paragraph.
Hixie's avatar
Hixie committed
119 120 121 122
///
/// 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.
123 124 125
///
/// 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
126
class TextPainter {
127 128
  /// Creates a text painter that paints the given text.
  ///
Ian Hickson's avatar
Ian Hickson committed
129 130 131 132
  /// 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.
133 134
  ///
  /// The [maxLines] property, if non-null, must be greater than zero.
135
  TextPainter({
136
    InlineSpan text,
137
    TextAlign textAlign = TextAlign.start,
Ian Hickson's avatar
Ian Hickson committed
138
    TextDirection textDirection,
139
    double textScaleFactor = 1.0,
140
    int maxLines,
141
    String ellipsis,
142
    Locale locale,
143
    StrutStyle strutStyle,
144
    TextWidthBasis textWidthBasis = TextWidthBasis.parent,
145
    ui.TextHeightBehavior textHeightBehavior,
146
  }) : assert(text == null || text.debugAssertIsValid()),
Ian Hickson's avatar
Ian Hickson committed
147
       assert(textAlign != null),
148
       assert(textScaleFactor != null),
149
       assert(maxLines == null || maxLines > 0),
150
       assert(textWidthBasis != null),
151 152
       _text = text,
       _textAlign = textAlign,
Ian Hickson's avatar
Ian Hickson committed
153
       _textDirection = textDirection,
154 155
       _textScaleFactor = textScaleFactor,
       _maxLines = maxLines,
156
       _ellipsis = ellipsis,
157
       _locale = locale,
158
       _strutStyle = strutStyle,
159 160
       _textWidthBasis = textWidthBasis,
       _textHeightBehavior = textHeightBehavior;
161

162
  ui.Paragraph _paragraph;
163
  bool _needsLayout = true;
164

165 166 167 168 169 170 171 172 173
  /// Marks this text painter's layout information as dirty and removes cached
  /// information.
  ///
  /// Uses this method to notify text painter to relayout in the case of
  /// layout changes in engine. In most cases, updating text painter properties
  /// in framework will automatically invoke this method.
  void markNeedsLayout() {
    _paragraph = null;
    _needsLayout = true;
174 175
    _previousCaretPosition = null;
    _previousCaretPrototype = null;
176 177
  }

Florian Loitsch's avatar
Florian Loitsch committed
178
  /// The (potentially styled) text to paint.
179 180
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
181
  /// This and [textDirection] must be non-null before you call [layout].
182 183
  ///
  /// The [InlineSpan] this provides is in the form of a tree that may contain
184
  /// multiple instances of [TextSpan]s and [WidgetSpan]s. To obtain a plain text
185 186 187 188 189 190
  /// representation of the contents of this [TextPainter], use [InlineSpan.toPlainText]
  /// to get the full contents of all nodes in the tree. [TextSpan.text] will
  /// only provide the contents of the first node in the tree.
  InlineSpan get text => _text;
  InlineSpan _text;
  set text(InlineSpan value) {
191
    assert(value == null || value.debugAssertIsValid());
192 193
    if (_text == value)
      return;
194 195
    if (_text?.style != value?.style)
      _layoutTemplate = null;
196
    _text = value;
197
    markNeedsLayout();
198 199 200
  }

  /// How the text should be aligned horizontally.
201 202
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
Ian Hickson's avatar
Ian Hickson committed
203 204
  ///
  /// The [textAlign] property must not be null. It defaults to [TextAlign.start].
205 206
  TextAlign get textAlign => _textAlign;
  TextAlign _textAlign;
207
  set textAlign(TextAlign value) {
Ian Hickson's avatar
Ian Hickson committed
208
    assert(value != null);
209 210 211
    if (_textAlign == value)
      return;
    _textAlign = value;
212
    markNeedsLayout();
213 214
  }

Ian Hickson's avatar
Ian Hickson committed
215 216 217 218 219 220 221 222 223
  /// 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]
224
  /// context, the English phrase will be on the right and the Hebrew phrase on
Ian Hickson's avatar
Ian Hickson committed
225 226 227 228 229 230 231 232 233 234 235
  /// 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;
236
    markNeedsLayout();
Ian Hickson's avatar
Ian Hickson committed
237 238 239
    _layoutTemplate = null; // Shouldn't really matter, but for strict correctness...
  }

240 241 242 243
  /// 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.
244 245
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
246 247 248 249 250 251 252
  double get textScaleFactor => _textScaleFactor;
  double _textScaleFactor;
  set textScaleFactor(double value) {
    assert(value != null);
    if (_textScaleFactor == value)
      return;
    _textScaleFactor = value;
253
    markNeedsLayout();
254
    _layoutTemplate = null;
255 256
  }

257
  /// The string used to ellipsize overflowing text. Setting this to a non-empty
258
  /// string will cause this string to be substituted for the remaining text
259 260 261 262 263 264 265
  /// 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].
266 267
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
268 269 270 271 272
  ///
  /// 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 (…).
273 274 275 276 277 278 279
  String get ellipsis => _ellipsis;
  String _ellipsis;
  set ellipsis(String value) {
    assert(value == null || value.isNotEmpty);
    if (_ellipsis == value)
      return;
    _ellipsis = value;
280
    markNeedsLayout();
281 282
  }

283
  /// The locale used to select region-specific glyphs.
284 285 286
  Locale get locale => _locale;
  Locale _locale;
  set locale(Locale value) {
287 288 289
    if (_locale == value)
      return;
    _locale = value;
290
    markNeedsLayout();
291 292
  }

293 294
  /// An optional maximum number of lines for the text to span, wrapping if
  /// necessary.
295
  ///
296 297
  /// If the text exceeds the given number of lines, it is truncated such that
  /// subsequent lines are dropped.
298 299
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
300 301
  int get maxLines => _maxLines;
  int _maxLines;
302
  /// The value may be null. If it is not null, then it must be greater than zero.
303
  set maxLines(int value) {
304
    assert(value == null || value > 0);
305 306 307
    if (_maxLines == value)
      return;
    _maxLines = value;
308
    markNeedsLayout();
309 310
  }

311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
  /// {@template flutter.painting.textPainter.strutStyle}
  /// The strut style to use. Strut style defines the strut, which sets minimum
  /// vertical layout metrics.
  ///
  /// Omitting or providing null will disable strut.
  ///
  /// Omitting or providing null for any properties of [StrutStyle] will result in
  /// default values being used. It is highly recommended to at least specify a
  /// [fontSize].
  ///
  /// See [StrutStyle] for details.
  /// {@endtemplate}
  StrutStyle get strutStyle => _strutStyle;
  StrutStyle _strutStyle;
  set strutStyle(StrutStyle value) {
    if (_strutStyle == value)
      return;
    _strutStyle = value;
329
    markNeedsLayout();
330 331
  }

332 333 334
  /// {@template flutter.painting.textPainter.textWidthBasis}
  /// Defines how to measure the width of the rendered text.
  /// {@endtemplate}
335 336 337 338 339 340 341
  TextWidthBasis get textWidthBasis => _textWidthBasis;
  TextWidthBasis _textWidthBasis;
  set textWidthBasis(TextWidthBasis value) {
    assert(value != null);
    if (_textWidthBasis == value)
      return;
    _textWidthBasis = value;
342
    markNeedsLayout();
343 344
  }

345 346 347 348 349 350 351 352 353
  /// {@macro flutter.dart:ui.textHeightBehavior}
  ui.TextHeightBehavior get textHeightBehavior => _textHeightBehavior;
  ui.TextHeightBehavior _textHeightBehavior;
  set textHeightBehavior(ui.TextHeightBehavior value) {
    if (_textHeightBehavior == value)
      return;
    _textHeightBehavior = value;
    markNeedsLayout();
  }
354

355
  ui.Paragraph _layoutTemplate;
356

357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
  /// An ordered list of [TextBox]es that bound the positions of the placeholders
  /// in the paragraph.
  ///
  /// Each box corresponds to a [PlaceholderSpan] in the order they were defined
  /// in the [InlineSpan] tree.
  List<TextBox> get inlinePlaceholderBoxes => _inlinePlaceholderBoxes;
  List<TextBox> _inlinePlaceholderBoxes;

  /// An ordered list of scales for each placeholder in the paragraph.
  ///
  /// The scale is used as a multiplier on the height, width and baselineOffset of
  /// the placeholder. Scale is primarily used to handle accessibility scaling.
  ///
  /// Each scale corresponds to a [PlaceholderSpan] in the order they were defined
  /// in the [InlineSpan] tree.
  List<double> get inlinePlaceholderScales => _inlinePlaceholderScales;
  List<double> _inlinePlaceholderScales;

  /// Sets the dimensions of each placeholder in [text].
  ///
  /// The number of [PlaceholderDimensions] provided should be the same as the
  /// number of [PlaceholderSpan]s in text. Passing in an empty or null `value`
  /// will do nothing.
  ///
  /// If [layout] is attempted without setting the placeholder dimensions, the
  /// placeholders will be ignored in the text layout and no valid
  /// [inlinePlaceholderBoxes] will be returned.
  void setPlaceholderDimensions(List<PlaceholderDimensions> value) {
    if (value == null || value.isEmpty || listEquals(value, _placeholderDimensions)) {
      return;
    }
    assert(() {
      int placeholderCount = 0;
      text.visitChildren((InlineSpan span) {
        if (span is PlaceholderSpan) {
          placeholderCount += 1;
        }
        return true;
      });
      return placeholderCount;
    }() == value.length);
    _placeholderDimensions = value;
399
    markNeedsLayout();
400 401 402
  }
  List<PlaceholderDimensions> _placeholderDimensions;

403
  ui.ParagraphStyle _createParagraphStyle([ TextDirection defaultTextDirection ]) {
Ian Hickson's avatar
Ian Hickson committed
404 405 406 407
    // 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.');
408 409
    return _text.style?.getParagraphStyle(
      textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
410
      textDirection: textDirection ?? defaultTextDirection,
411 412
      textScaleFactor: textScaleFactor,
      maxLines: _maxLines,
413
      textHeightBehavior: _textHeightBehavior,
414
      ellipsis: _ellipsis,
415
      locale: _locale,
416
      strutStyle: _strutStyle,
417
    ) ?? ui.ParagraphStyle(
418
      textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
419
      textDirection: textDirection ?? defaultTextDirection,
420
      maxLines: maxLines,
421
      textHeightBehavior: _textHeightBehavior,
422
      ellipsis: ellipsis,
423
      locale: locale,
424 425 426
    );
  }

427
  /// The height of a space in [text] in logical pixels.
428
  ///
429 430
  /// 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
431
  /// relative a typical line of text.
432 433
  ///
  /// Obtaining this value does not require calling [layout].
Ian Hickson's avatar
Ian Hickson committed
434 435 436 437 438
  ///
  /// 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).
439 440
  double get preferredLineHeight {
    if (_layoutTemplate == null) {
441
      final ui.ParagraphBuilder builder = ui.ParagraphBuilder(
Ian Hickson's avatar
Ian Hickson committed
442
        _createParagraphStyle(TextDirection.rtl),
443
      ); // direction doesn't matter, text is just a space
Ian Hickson's avatar
Ian Hickson committed
444
      if (text?.style != null)
445
        builder.pushStyle(text.style.getTextStyle(textScaleFactor: textScaleFactor));
446
      builder.addText(' ');
447
      _layoutTemplate = builder.build()
448
        ..layout(const ui.ParagraphConstraints(width: double.infinity));
449 450 451
    }
    return _layoutTemplate.height;
  }
452

453 454 455 456 457 458 459 460 461 462 463
  // 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();
  }

464 465
  /// The width at which decreasing the width of the text would prevent it from
  /// painting itself completely within its bounds.
466
  ///
467
  /// Valid only after [layout] has been called.
468
  double get minIntrinsicWidth {
469 470 471 472
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.minIntrinsicWidth);
  }

Florian Loitsch's avatar
Florian Loitsch committed
473
  /// The width at which increasing the width of the text no longer decreases the height.
474
  ///
475
  /// Valid only after [layout] has been called.
476
  double get maxIntrinsicWidth {
477 478 479 480
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.maxIntrinsicWidth);
  }

481 482
  /// The horizontal space required to paint this text.
  ///
483
  /// Valid only after [layout] has been called.
484 485
  double get width {
    assert(!_needsLayout);
486 487 488
    return _applyFloatingPointHack(
      textWidthBasis == TextWidthBasis.longestLine ? _paragraph.longestLine : _paragraph.width,
    );
489 490
  }

491 492
  /// The vertical space required to paint this text.
  ///
493
  /// Valid only after [layout] has been called.
494 495 496 497 498
  double get height {
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.height);
  }

499 500
  /// The amount of space required to paint this text.
  ///
501
  /// Valid only after [layout] has been called.
502
  Size get size {
503
    assert(!_needsLayout);
504
    return Size(width, height);
505 506
  }

507 508
  /// Returns the distance from the top of the text to the first baseline of the
  /// given type.
509
  ///
510
  /// Valid only after [layout] has been called.
511 512
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    assert(!_needsLayout);
Ian Hickson's avatar
Ian Hickson committed
513
    assert(baseline != null);
514 515
    switch (baseline) {
      case TextBaseline.alphabetic:
516
        return _paragraph.alphabeticBaseline;
517
      case TextBaseline.ideographic:
518
        return _paragraph.ideographicBaseline;
519
    }
pq's avatar
pq committed
520
    return null;
521 522
  }

523 524 525 526 527
  /// 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.
528
  ///
529 530 531 532 533
  /// 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.
534 535 536 537 538
  bool get didExceedMaxLines {
    assert(!_needsLayout);
    return _paragraph.didExceedMaxLines;
  }

539 540
  double _lastMinWidth;
  double _lastMaxWidth;
541

Florian Loitsch's avatar
Florian Loitsch committed
542
  /// Computes the visual position of the glyphs for painting the text.
543
  ///
544
  /// The text will layout with a width that's as close to its max intrinsic
Ian Hickson's avatar
Ian Hickson committed
545 546 547 548 549
  /// 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.
550
  void layout({ double minWidth = 0.0, double maxWidth = double.infinity }) {
Ian Hickson's avatar
Ian Hickson committed
551 552
    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.');
553
    if (!_needsLayout && minWidth == _lastMinWidth && maxWidth == _lastMaxWidth)
554 555
      return;
    _needsLayout = false;
556
    if (_paragraph == null) {
557
      final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle());
558 559
      _text.build(builder, textScaleFactor: textScaleFactor, dimensions: _placeholderDimensions);
      _inlinePlaceholderScales = builder.placeholderScales;
560
      _paragraph = builder.build();
561
    }
562 563
    _lastMinWidth = minWidth;
    _lastMaxWidth = maxWidth;
564
    _paragraph.layout(ui.ParagraphConstraints(width: maxWidth));
565
    if (minWidth != maxWidth) {
566
      final double newWidth = maxIntrinsicWidth.clamp(minWidth, maxWidth) as double;
567
      if (newWidth != width) {
568
        _paragraph.layout(ui.ParagraphConstraints(width: newWidth));
569
      }
570
    }
571
    _inlinePlaceholderBoxes = _paragraph.getBoxesForPlaceholders();
572 573
  }

Florian Loitsch's avatar
Florian Loitsch committed
574
  /// Paints the text onto the given canvas at the given offset.
575
  ///
576
  /// Valid only after [layout] has been called.
577 578 579 580 581 582 583 584 585
  ///
  /// 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.
586
  void paint(Canvas canvas, Offset offset) {
587 588
    assert(() {
      if (_needsLayout) {
589
        throw FlutterError(
590 591 592 593 594
          '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;
595
    }());
Adam Barth's avatar
Adam Barth committed
596
    canvas.drawParagraph(_paragraph, offset);
597
  }
598

599 600
  // Complex glyphs can be represented by two or more UTF16 codepoints. This
  // checks if the value represents a UTF16 glyph by itself or is a 'surrogate'.
601 602 603 604
  bool _isUtf16Surrogate(int value) {
    return value & 0xF800 == 0xD800;
  }

605 606 607 608 609 610 611 612
  // Checks if the glyph is either [Unicode.RLM] or [Unicode.LRM]. These values take
  // up zero space and do not have valid bounding boxes around them.
  //
  // We do not directly use the [Unicode] constants since they are strings.
  bool _isUnicodeDirectionality(int value) {
    return value == 0x200F || value == 0x200E;
  }

613
  /// Returns the closest offset after `offset` at which the input cursor can be
614 615 616 617 618
  /// positioned.
  int getOffsetAfter(int offset) {
    final int nextCodeUnit = _text.codeUnitAt(offset);
    if (nextCodeUnit == null)
      return null;
619
    // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
620 621 622
    return _isUtf16Surrogate(nextCodeUnit) ? offset + 2 : offset + 1;
  }

623
  /// Returns the closest offset before `offset` at which the input cursor can
624 625 626 627 628
  /// be positioned.
  int getOffsetBefore(int offset) {
    final int prevCodeUnit = _text.codeUnitAt(offset - 1);
    if (prevCodeUnit == null)
      return null;
629
    // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
630 631 632
    return _isUtf16Surrogate(prevCodeUnit) ? offset - 2 : offset - 1;
  }

633 634 635
  // Unicode value for a zero width joiner character.
  static const int _zwjUtf16 = 0x200d;

636
  // Get the Rect of the cursor (in logical pixels) based off the near edge
637
  // of the character upstream from the given string offset.
638 639
  // TODO(garyq): Use actual extended grapheme cluster length instead of
  // an increasing cluster length amount to achieve deterministic performance.
640
  Rect _getRectFromUpstream(int offset, Rect caretPrototype) {
641
    final String flattenedText = _text.toPlainText(includePlaceholders: false);
642
    final int prevCodeUnit = _text.codeUnitAt(max(0, offset - 1));
643 644
    if (prevCodeUnit == null)
      return null;
645

646
    // Check for multi-code-unit glyphs such as emojis or zero width joiner
647
    final bool needsSearch = _isUtf16Surrogate(prevCodeUnit) || _text.codeUnitAt(offset) == _zwjUtf16 || _isUnicodeDirectionality(prevCodeUnit);
648 649
    int graphemeClusterLength = needsSearch ? 2 : 1;
    List<TextBox> boxes = <TextBox>[];
650
    while (boxes.isEmpty && flattenedText != null) {
651
      final int prevRuneOffset = offset - graphemeClusterLength;
652 653 654
      // Use BoxHeightStyle.strut to ensure that the caret's height fits within
      // the line's height and is consistent throughout the line.
      boxes = _paragraph.getBoxesForRange(prevRuneOffset, offset, boxHeightStyle: ui.BoxHeightStyle.strut);
655 656 657 658
      // When the range does not include a full cluster, no boxes will be returned.
      if (boxes.isEmpty) {
        // When we are at the beginning of the line, a non-surrogate position will
        // return empty boxes. We break and try from downstream instead.
659
        if (!needsSearch) {
660
          break; // Only perform one iteration if no search is required.
661 662
        }
        if (prevRuneOffset < -flattenedText.length) {
663
          break; // Stop iterating when beyond the max length of the text.
664
        }
665 666 667 668 669 670 671
        // Multiply by two to log(n) time cover the entire text span. This allows
        // faster discovery of very long clusters and reduces the possibility
        // of certain large clusters taking much longer than others, which can
        // cause jank.
        graphemeClusterLength *= 2;
        continue;
      }
672
      final TextBox box = boxes.first;
673 674 675 676

      // If the upstream character is a newline, cursor is at start of next line
      const int NEWLINE_CODE_UNIT = 10;
      if (prevCodeUnit == NEWLINE_CODE_UNIT) {
677
        return Rect.fromLTRB(_emptyOffset.dx, box.bottom, _emptyOffset.dx, box.bottom + box.bottom - box.top);
678 679
      }

680 681
      final double caretEnd = box.end;
      final double dx = box.direction == TextDirection.rtl ? caretEnd - caretPrototype.width : caretEnd;
682
      return Rect.fromLTRB(min(dx, _paragraph.width), box.top, min(dx, _paragraph.width), box.bottom);
683 684
    }
    return null;
685 686
  }

687
  // Get the Rect of the cursor (in logical pixels) based off the near edge
688
  // of the character downstream from the given string offset.
689 690
  // TODO(garyq): Use actual extended grapheme cluster length instead of
  // an increasing cluster length amount to achieve deterministic performance.
691
  Rect _getRectFromDownstream(int offset, Rect caretPrototype) {
692
    final String flattenedText = _text.toPlainText(includePlaceholders: false);
693
    // We cap the offset at the final index of the _text.
694
    final int nextCodeUnit = _text.codeUnitAt(min(offset, flattenedText == null ? 0 : flattenedText.length - 1));
695 696
    if (nextCodeUnit == null)
      return null;
697
    // Check for multi-code-unit glyphs such as emojis or zero width joiner
698
    final bool needsSearch = _isUtf16Surrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16 || _isUnicodeDirectionality(nextCodeUnit);
699 700
    int graphemeClusterLength = needsSearch ? 2 : 1;
    List<TextBox> boxes = <TextBox>[];
701
    while (boxes.isEmpty && flattenedText != null) {
702
      final int nextRuneOffset = offset + graphemeClusterLength;
703 704 705
      // Use BoxHeightStyle.strut to ensure that the caret's height fits within
      // the line's height and is consistent throughout the line.
      boxes = _paragraph.getBoxesForRange(offset, nextRuneOffset, boxHeightStyle: ui.BoxHeightStyle.strut);
706 707 708 709
      // When the range does not include a full cluster, no boxes will be returned.
      if (boxes.isEmpty) {
        // When we are at the end of the line, a non-surrogate position will
        // return empty boxes. We break and try from upstream instead.
710
        if (!needsSearch) {
711
          break; // Only perform one iteration if no search is required.
712 713
        }
        if (nextRuneOffset >= flattenedText.length << 1) {
714
          break; // Stop iterating when beyond the max length of the text.
715
        }
716 717 718 719 720 721 722
        // Multiply by two to log(n) time cover the entire text span. This allows
        // faster discovery of very long clusters and reduces the possibility
        // of certain large clusters taking much longer than others, which can
        // cause jank.
        graphemeClusterLength *= 2;
        continue;
      }
723
      final TextBox box = boxes.last;
724 725
      final double caretStart = box.start;
      final double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
726
      return Rect.fromLTRB(min(dx, _paragraph.width), box.top, min(dx, _paragraph.width), box.bottom);
727 728
    }
    return null;
729 730
  }

731
  Offset get _emptyOffset {
Ian Hickson's avatar
Ian Hickson committed
732 733 734
    assert(!_needsLayout); // implies textDirection is non-null
    assert(textAlign != null);
    switch (textAlign) {
735 736 737
      case TextAlign.left:
        return Offset.zero;
      case TextAlign.right:
738
        return Offset(width, 0.0);
739
      case TextAlign.center:
740
        return Offset(width / 2.0, 0.0);
Ian Hickson's avatar
Ian Hickson committed
741 742 743 744 745
      case TextAlign.justify:
      case TextAlign.start:
        assert(textDirection != null);
        switch (textDirection) {
          case TextDirection.rtl:
746
            return Offset(width, 0.0);
Ian Hickson's avatar
Ian Hickson committed
747 748 749 750 751 752 753 754 755 756
          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:
757
            return Offset(width, 0.0);
Ian Hickson's avatar
Ian Hickson committed
758 759
        }
        return null;
760 761 762 763
    }
    return null;
  }

764
  /// Returns the offset at which to paint the caret.
765
  ///
766
  /// Valid only after [layout] has been called.
767
  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786
    _computeCaretMetrics(position, caretPrototype);
    return _caretMetrics.offset;
  }

  /// Returns the tight bounded height of the glyph at the given [position].
  ///
  /// Valid only after [layout] has been called.
  double getFullHeightForCaret(TextPosition position, Rect caretPrototype) {
    _computeCaretMetrics(position, caretPrototype);
    return _caretMetrics.fullHeight;
  }

  // Cached caret metrics. This allows multiple invokes of [getOffsetForCaret] and
  // [getFullHeightForCaret] in a row without performing redundant and expensive
  // get rect calls to the paragraph.
  _CaretMetrics _caretMetrics;

  // Holds the TextPosition and caretPrototype the last caret metrics were
  // computed with. When new values are passed in, we recompute the caret metrics.
Chris Bracken's avatar
Chris Bracken committed
787
  // only as necessary.
788 789 790 791 792 793
  TextPosition _previousCaretPosition;
  Rect _previousCaretPrototype;

  // Checks if the [position] and [caretPrototype] have changed from the cached
  // version and recomputes the metrics required to position the caret.
  void _computeCaretMetrics(TextPosition position, Rect caretPrototype) {
794
    assert(!_needsLayout);
795 796
    if (position == _previousCaretPosition && caretPrototype == _previousCaretPrototype)
      return;
797
    final int offset = position.offset;
Ian Hickson's avatar
Ian Hickson committed
798
    assert(position.affinity != null);
799
    Rect rect;
800
    switch (position.affinity) {
801 802 803 804 805 806 807 808
      case TextAffinity.upstream: {
        rect = _getRectFromUpstream(offset, caretPrototype) ?? _getRectFromDownstream(offset, caretPrototype);
        break;
      }
      case TextAffinity.downstream: {
        rect = _getRectFromDownstream(offset, caretPrototype) ??  _getRectFromUpstream(offset, caretPrototype);
        break;
      }
809
    }
810 811 812 813
    _caretMetrics = _CaretMetrics(
      offset: rect != null ? Offset(rect.left, rect.top) : _emptyOffset,
      fullHeight: rect != null ? rect.bottom - rect.top : null,
    );
814 815 816 817

    // Cache the input parameters to prevent repeat work later.
    _previousCaretPosition = position;
    _previousCaretPrototype = caretPrototype;
818 819 820 821
  }

  /// Returns a list of rects that bound the given selection.
  ///
822 823 824 825 826
  /// The [boxHeightStyle] and [boxWidthStyle] arguments may be used to select
  /// the shape of the [TextBox]s. These properties default to
  /// [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and
  /// must not be null.
  ///
827 828 829
  /// 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.
830 831 832 833 834
  List<TextBox> getBoxesForSelection(
    TextSelection selection, {
    ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
    ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
  }) {
835
    assert(!_needsLayout);
836 837 838 839 840 841 842 843
    assert(boxHeightStyle != null);
    assert(boxWidthStyle != null);
    return _paragraph.getBoxesForRange(
      selection.start,
      selection.end,
      boxHeightStyle: boxHeightStyle,
      boxWidthStyle: boxWidthStyle
    );
844 845
  }

846
  /// Returns the position within the text for the given pixel offset.
847 848 849 850 851
  TextPosition getPositionForOffset(Offset offset) {
    assert(!_needsLayout);
    return _paragraph.getPositionForOffset(offset);
  }

852 853 854 855 856 857 858
  /// 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>.
859 860
  TextRange getWordBoundary(TextPosition position) {
    assert(!_needsLayout);
861 862 863
    return _paragraph.getWordBoundary(position);
  }

864
  /// Returns the text range of the line at the given offset.
865
  ///
866
  /// The newline, if any, is included in the range.
867 868 869
  TextRange getLineBoundary(TextPosition position) {
    assert(!_needsLayout);
    return _paragraph.getLineBoundary(position);
870
  }
871 872 873 874 875 876 877 878 879 880 881 882 883 884 885

  /// Returns the full list of [LineMetrics] that describe in detail the various
  /// metrics of each laid out line.
  ///
  /// The [LineMetrics] list is presented in the order of the lines they represent.
  /// For example, the first line is in the zeroth index.
  ///
  /// [LineMetrics] contains measurements such as ascent, descent, baseline, and
  /// width for the line as a whole, and may be useful for aligning additional
  /// widgets to a particular line.
  ///
  /// Valid only after [layout] has been called.
  ///
  /// This can potentially return a large amount of data, so it is not recommended
  /// to repeatedly call this. Instead, cache the results. The cached results
886
  /// should be invalidated upon the next successful [layout].
887 888 889 890
  List<ui.LineMetrics> computeLineMetrics() {
    assert(!_needsLayout);
    return _paragraph.computeLineMetrics();
  }
891
}