text_painter.dart 36.3 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
// The default font size if none is specified. This should be kept in
// sync with the default values in text_style.dart, as well as the
// defaults set in the engine (eg, LibTxt's text_style.h, paragraph_style.h).
const double _kDefaultFontSize = 14.0;

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
/// How overflowing text should be handled.
///
/// A [TextOverflow] can be passed to [Text] and [RichText] via their
/// [Text.overflow] and [RichText.overflow] properties respectively.
enum TextOverflow {
  /// Clip the overflowing text to fix its container.
  clip,

  /// Fade the overflowing text to transparent.
  fade,

  /// Use an ellipsis to indicate that the text has overflowed.
  ellipsis,

  /// Render overflowing text outside of its container.
  visible,
}

42 43 44 45 46 47 48 49 50 51 52 53 54
/// 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.
55
///  * [RichText], a text widget that supports text inline widgets.
56 57 58 59 60 61 62
@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({
63 64
    required this.size,
    required this.alignment,
65 66 67 68 69
    this.baseline,
    this.baselineOffset,
  }) : assert(size != null),
       assert(alignment != null);

70 71 72
  /// A constant representing an empty placeholder.
  static const PlaceholderDimensions empty = PlaceholderDimensions(size: Size.zero, alignment: ui.PlaceholderAlignment.bottom);

73 74 75 76 77 78 79 80
  /// 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
81 82 83
  ///    [dart:ui.PlaceholderAlignment.baseline],
  ///    [dart:ui.PlaceholderAlignment.aboveBaseline],
  ///    or [dart:ui.PlaceholderAlignment.belowBaseline].
84 85 86 87 88 89 90
  ///  * [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].
91
  final double? baselineOffset;
92 93 94 95 96

  /// The [TextBaseline] to align to. Used with:
  ///
  ///  * [ui.PlaceholderAlignment.baseline]
  ///  * [ui.PlaceholderAlignment.aboveBaseline]
97
  ///  * [ui.PlaceholderAlignment.belowBaseline]
98
  ///  * [ui.PlaceholderAlignment.middle]
99
  final TextBaseline? baseline;
100 101 102 103 104 105 106

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

107
/// The different ways of measuring the width of one or more lines of text.
108
///
109
/// See [Text.textWidthBasis], for example.
110
enum TextWidthBasis {
111
  /// multiline text will take up the full width given by the parent. For single
112 113 114 115 116 117 118 119 120 121
  /// 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,
}

122 123 124
/// 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.
125
class _CaretMetrics {
126
  const _CaretMetrics({required this.offset, this.fullHeight});
127 128 129 130 131
  /// 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.
132
  final double? fullHeight;
133 134
}

135
/// An object that paints a [TextSpan] tree into a [Canvas].
Hixie's avatar
Hixie committed
136 137 138 139 140 141
///
/// To use a [TextPainter], follow these steps:
///
/// 1. Create a [TextSpan] tree and pass it to the [TextPainter]
///    constructor.
///
142
/// 2. Call [layout] to prepare the paragraph.
Hixie's avatar
Hixie committed
143
///
144
/// 3. Call [paint] as often as desired to paint the paragraph.
Hixie's avatar
Hixie committed
145 146 147 148
///
/// 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.
149 150 151
///
/// 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
152
class TextPainter {
153 154
  /// Creates a text painter that paints the given text.
  ///
Ian Hickson's avatar
Ian Hickson committed
155 156 157 158
  /// 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.
159 160
  ///
  /// The [maxLines] property, if non-null, must be greater than zero.
161
  TextPainter({
162
    InlineSpan? text,
163
    TextAlign textAlign = TextAlign.start,
164
    TextDirection? textDirection,
165
    double textScaleFactor = 1.0,
166 167 168 169
    int? maxLines,
    String? ellipsis,
    Locale? locale,
    StrutStyle? strutStyle,
170
    TextWidthBasis textWidthBasis = TextWidthBasis.parent,
171
    ui.TextHeightBehavior? textHeightBehavior,
172
  }) : assert(text == null || text.debugAssertIsValid()),
Ian Hickson's avatar
Ian Hickson committed
173
       assert(textAlign != null),
174
       assert(textScaleFactor != null),
175
       assert(maxLines == null || maxLines > 0),
176
       assert(textWidthBasis != null),
177 178
       _text = text,
       _textAlign = textAlign,
Ian Hickson's avatar
Ian Hickson committed
179
       _textDirection = textDirection,
180 181
       _textScaleFactor = textScaleFactor,
       _maxLines = maxLines,
182
       _ellipsis = ellipsis,
183
       _locale = locale,
184
       _strutStyle = strutStyle,
185 186
       _textWidthBasis = textWidthBasis,
       _textHeightBehavior = textHeightBehavior;
187

188
  ui.Paragraph? _paragraph;
189
  bool _needsLayout = true;
190

191 192 193 194 195 196 197 198 199
  /// 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;
200 201
    _previousCaretPosition = null;
    _previousCaretPrototype = null;
202 203
  }

Florian Loitsch's avatar
Florian Loitsch committed
204
  /// The (potentially styled) text to paint.
205 206
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
207
  /// This and [textDirection] must be non-null before you call [layout].
208 209
  ///
  /// The [InlineSpan] this provides is in the form of a tree that may contain
210
  /// multiple instances of [TextSpan]s and [WidgetSpan]s. To obtain a plain text
211 212 213
  /// 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.
214 215 216
  InlineSpan? get text => _text;
  InlineSpan? _text;
  set text(InlineSpan? value) {
217
    assert(value == null || value.debugAssertIsValid());
218 219
    if (_text == value)
      return;
220 221
    if (_text?.style != value?.style)
      _layoutTemplate = null;
222
    _text = value;
223
    markNeedsLayout();
224 225 226
  }

  /// How the text should be aligned horizontally.
227 228
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
Ian Hickson's avatar
Ian Hickson committed
229 230
  ///
  /// The [textAlign] property must not be null. It defaults to [TextAlign.start].
231 232
  TextAlign get textAlign => _textAlign;
  TextAlign _textAlign;
233
  set textAlign(TextAlign value) {
Ian Hickson's avatar
Ian Hickson committed
234
    assert(value != null);
235 236 237
    if (_textAlign == value)
      return;
    _textAlign = value;
238
    markNeedsLayout();
239 240
  }

Ian Hickson's avatar
Ian Hickson committed
241 242 243 244 245 246 247 248 249
  /// 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]
250
  /// context, the English phrase will be on the right and the Hebrew phrase on
Ian Hickson's avatar
Ian Hickson committed
251 252 253 254 255
  /// 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].
256 257 258
  TextDirection? get textDirection => _textDirection;
  TextDirection? _textDirection;
  set textDirection(TextDirection? value) {
Ian Hickson's avatar
Ian Hickson committed
259 260 261
    if (_textDirection == value)
      return;
    _textDirection = value;
262
    markNeedsLayout();
Ian Hickson's avatar
Ian Hickson committed
263 264 265
    _layoutTemplate = null; // Shouldn't really matter, but for strict correctness...
  }

266 267 268 269
  /// 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.
270 271
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
272 273 274 275 276 277 278
  double get textScaleFactor => _textScaleFactor;
  double _textScaleFactor;
  set textScaleFactor(double value) {
    assert(value != null);
    if (_textScaleFactor == value)
      return;
    _textScaleFactor = value;
279
    markNeedsLayout();
280
    _layoutTemplate = null;
281 282
  }

283
  /// The string used to ellipsize overflowing text. Setting this to a non-empty
284
  /// string will cause this string to be substituted for the remaining text
285 286 287 288 289 290 291
  /// 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].
292 293
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
294 295 296 297 298
  ///
  /// 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 (…).
299 300 301
  String? get ellipsis => _ellipsis;
  String? _ellipsis;
  set ellipsis(String? value) {
302 303 304 305
    assert(value == null || value.isNotEmpty);
    if (_ellipsis == value)
      return;
    _ellipsis = value;
306
    markNeedsLayout();
307 308
  }

309
  /// The locale used to select region-specific glyphs.
310 311 312
  Locale? get locale => _locale;
  Locale? _locale;
  set locale(Locale? value) {
313 314 315
    if (_locale == value)
      return;
    _locale = value;
316
    markNeedsLayout();
317 318
  }

319 320
  /// An optional maximum number of lines for the text to span, wrapping if
  /// necessary.
321
  ///
322 323
  /// If the text exceeds the given number of lines, it is truncated such that
  /// subsequent lines are dropped.
324 325
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
326 327
  int? get maxLines => _maxLines;
  int? _maxLines;
328
  /// The value may be null. If it is not null, then it must be greater than zero.
329
  set maxLines(int? value) {
330
    assert(value == null || value > 0);
331 332 333
    if (_maxLines == value)
      return;
    _maxLines = value;
334
    markNeedsLayout();
335 336
  }

337 338 339 340 341 342 343 344
  /// {@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
345
  /// [StrutStyle.fontSize].
346 347 348
  ///
  /// See [StrutStyle] for details.
  /// {@endtemplate}
349 350 351
  StrutStyle? get strutStyle => _strutStyle;
  StrutStyle? _strutStyle;
  set strutStyle(StrutStyle? value) {
352 353 354
    if (_strutStyle == value)
      return;
    _strutStyle = value;
355
    markNeedsLayout();
356 357
  }

358 359 360
  /// {@template flutter.painting.textPainter.textWidthBasis}
  /// Defines how to measure the width of the rendered text.
  /// {@endtemplate}
361 362 363 364 365 366 367
  TextWidthBasis get textWidthBasis => _textWidthBasis;
  TextWidthBasis _textWidthBasis;
  set textWidthBasis(TextWidthBasis value) {
    assert(value != null);
    if (_textWidthBasis == value)
      return;
    _textWidthBasis = value;
368
    markNeedsLayout();
369 370
  }

371
  /// {@macro flutter.dart:ui.textHeightBehavior}
372 373 374
  ui.TextHeightBehavior? get textHeightBehavior => _textHeightBehavior;
  ui.TextHeightBehavior? _textHeightBehavior;
  set textHeightBehavior(ui.TextHeightBehavior? value) {
375 376 377 378 379
    if (_textHeightBehavior == value)
      return;
    _textHeightBehavior = value;
    markNeedsLayout();
  }
380

381
  ui.Paragraph? _layoutTemplate;
382

383 384 385 386 387
  /// 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.
388 389
  List<TextBox>? get inlinePlaceholderBoxes => _inlinePlaceholderBoxes;
  List<TextBox>? _inlinePlaceholderBoxes;
390 391 392 393 394 395 396 397

  /// 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.
398 399
  List<double>? get inlinePlaceholderScales => _inlinePlaceholderScales;
  List<double>? _inlinePlaceholderScales;
400 401 402 403 404 405 406 407 408 409

  /// 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.
410
  void setPlaceholderDimensions(List<PlaceholderDimensions>? value) {
411 412 413 414 415
    if (value == null || value.isEmpty || listEquals(value, _placeholderDimensions)) {
      return;
    }
    assert(() {
      int placeholderCount = 0;
416
      text!.visitChildren((InlineSpan span) {
417 418 419 420 421 422 423 424
        if (span is PlaceholderSpan) {
          placeholderCount += 1;
        }
        return true;
      });
      return placeholderCount;
    }() == value.length);
    _placeholderDimensions = value;
425
    markNeedsLayout();
426
  }
427
  List<PlaceholderDimensions>? _placeholderDimensions;
428

429
  ui.ParagraphStyle _createParagraphStyle([ TextDirection? defaultTextDirection ]) {
Ian Hickson's avatar
Ian Hickson committed
430 431 432 433
    // 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.');
434
    return _text!.style?.getParagraphStyle(
435
      textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
436
      textDirection: textDirection ?? defaultTextDirection,
437 438
      textScaleFactor: textScaleFactor,
      maxLines: _maxLines,
439
      textHeightBehavior: _textHeightBehavior,
440
      ellipsis: _ellipsis,
441
      locale: _locale,
442
      strutStyle: _strutStyle,
443
    ) ?? ui.ParagraphStyle(
444
      textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
445
      textDirection: textDirection ?? defaultTextDirection,
446 447 448 449
      // Use the default font size to multiply by as RichText does not
      // perform inheriting [TextStyle]s and would otherwise
      // fail to apply textScaleFactor.
      fontSize: _kDefaultFontSize * textScaleFactor,
450
      maxLines: maxLines,
451
      textHeightBehavior: _textHeightBehavior,
452
      ellipsis: ellipsis,
453
      locale: locale,
454 455 456
    );
  }

457
  /// The height of a space in [text] in logical pixels.
458
  ///
459 460
  /// 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
461
  /// relative a typical line of text.
462 463
  ///
  /// Obtaining this value does not require calling [layout].
Ian Hickson's avatar
Ian Hickson committed
464 465 466 467 468
  ///
  /// 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).
469 470
  double get preferredLineHeight {
    if (_layoutTemplate == null) {
471
      final ui.ParagraphBuilder builder = ui.ParagraphBuilder(
Ian Hickson's avatar
Ian Hickson committed
472
        _createParagraphStyle(TextDirection.rtl),
473
      ); // direction doesn't matter, text is just a space
Ian Hickson's avatar
Ian Hickson committed
474
      if (text?.style != null)
475
        builder.pushStyle(text!.style!.getTextStyle(textScaleFactor: textScaleFactor));
476
      builder.addText(' ');
477
      _layoutTemplate = builder.build()
478
        ..layout(const ui.ParagraphConstraints(width: double.infinity));
479
    }
480
    return _layoutTemplate!.height;
481
  }
482

483 484 485 486 487 488 489 490 491 492 493
  // 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();
  }

494 495
  /// The width at which decreasing the width of the text would prevent it from
  /// painting itself completely within its bounds.
496
  ///
497
  /// Valid only after [layout] has been called.
498
  double get minIntrinsicWidth {
499
    assert(!_needsLayout);
500
    return _applyFloatingPointHack(_paragraph!.minIntrinsicWidth);
501 502
  }

Florian Loitsch's avatar
Florian Loitsch committed
503
  /// The width at which increasing the width of the text no longer decreases the height.
504
  ///
505
  /// Valid only after [layout] has been called.
506
  double get maxIntrinsicWidth {
507
    assert(!_needsLayout);
508
    return _applyFloatingPointHack(_paragraph!.maxIntrinsicWidth);
509 510
  }

511 512
  /// The horizontal space required to paint this text.
  ///
513
  /// Valid only after [layout] has been called.
514 515
  double get width {
    assert(!_needsLayout);
516
    return _applyFloatingPointHack(
517
      textWidthBasis == TextWidthBasis.longestLine ? _paragraph!.longestLine : _paragraph!.width,
518
    );
519 520
  }

521 522
  /// The vertical space required to paint this text.
  ///
523
  /// Valid only after [layout] has been called.
524 525
  double get height {
    assert(!_needsLayout);
526
    return _applyFloatingPointHack(_paragraph!.height);
527 528
  }

529 530
  /// The amount of space required to paint this text.
  ///
531
  /// Valid only after [layout] has been called.
532
  Size get size {
533
    assert(!_needsLayout);
534
    return Size(width, height);
535 536
  }

537 538
  /// Returns the distance from the top of the text to the first baseline of the
  /// given type.
539
  ///
540
  /// Valid only after [layout] has been called.
541 542
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    assert(!_needsLayout);
Ian Hickson's avatar
Ian Hickson committed
543
    assert(baseline != null);
544 545
    switch (baseline) {
      case TextBaseline.alphabetic:
546
        return _paragraph!.alphabeticBaseline;
547
      case TextBaseline.ideographic:
548
        return _paragraph!.ideographicBaseline;
549 550 551
    }
  }

552 553 554 555 556
  /// 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.
557
  ///
558 559 560 561 562
  /// 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.
563 564
  bool get didExceedMaxLines {
    assert(!_needsLayout);
565
    return _paragraph!.didExceedMaxLines;
566 567
  }

568 569
  double? _lastMinWidth;
  double? _lastMaxWidth;
570

Florian Loitsch's avatar
Florian Loitsch committed
571
  /// Computes the visual position of the glyphs for painting the text.
572
  ///
573
  /// The text will layout with a width that's as close to its max intrinsic
Ian Hickson's avatar
Ian Hickson committed
574 575 576 577 578
  /// 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.
579
  void layout({ double minWidth = 0.0, double maxWidth = double.infinity }) {
Ian Hickson's avatar
Ian Hickson committed
580 581
    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.');
582
    if (!_needsLayout && minWidth == _lastMinWidth && maxWidth == _lastMaxWidth)
583 584
      return;
    _needsLayout = false;
585
    if (_paragraph == null) {
586
      final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle());
587
      _text!.build(builder, textScaleFactor: textScaleFactor, dimensions: _placeholderDimensions);
588
      _inlinePlaceholderScales = builder.placeholderScales;
589
      _paragraph = builder.build();
590
    }
591 592
    _lastMinWidth = minWidth;
    _lastMaxWidth = maxWidth;
593 594 595
    // A change in layout invalidates the cached caret metrics as well.
    _previousCaretPosition = null;
    _previousCaretPrototype = null;
596
    _paragraph!.layout(ui.ParagraphConstraints(width: maxWidth));
597
    if (minWidth != maxWidth) {
598 599 600 601 602 603 604 605
      double newWidth;
      switch (textWidthBasis) {
        case TextWidthBasis.longestLine:
          // The parent widget expects the paragraph to be exactly
          // `TextPainter.width` wide, if that value satisfies the constraints
          // it gave to the TextPainter. So when `textWidthBasis` is longestLine,
          // the paragraph's width needs to be as close to the width of its
          // longest line as possible.
606
          newWidth = _applyFloatingPointHack(_paragraph!.longestLine);
607 608 609 610 611
          break;
        case TextWidthBasis.parent:
          newWidth = maxIntrinsicWidth;
          break;
      }
612
      newWidth = newWidth.clamp(minWidth, maxWidth);
613 614
      if (newWidth != _applyFloatingPointHack(_paragraph!.width)) {
        _paragraph!.layout(ui.ParagraphConstraints(width: newWidth));
615
      }
616
    }
617
    _inlinePlaceholderBoxes = _paragraph!.getBoxesForPlaceholders();
618 619
  }

Florian Loitsch's avatar
Florian Loitsch committed
620
  /// Paints the text onto the given canvas at the given offset.
621
  ///
622
  /// Valid only after [layout] has been called.
623 624 625 626 627 628 629 630 631
  ///
  /// 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.
632
  void paint(Canvas canvas, Offset offset) {
633 634
    assert(() {
      if (_needsLayout) {
635
        throw FlutterError(
636
          'TextPainter.paint called when text geometry was not yet calculated.\n'
637
          'Please call layout() before paint() to position the text before painting it.',
638 639 640
        );
      }
      return true;
641
    }());
642
    canvas.drawParagraph(_paragraph!, offset);
643
  }
644

645 646 647 648 649
  // Returns true iff the given value is a valid UTF-16 surrogate. The value
  // must be a UTF-16 code unit, meaning it must be in the range 0x0000-0xFFFF.
  //
  // See also:
  //   * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
650
  static bool _isUtf16Surrogate(int value) {
651 652 653
    return value & 0xF800 == 0xD800;
  }

654 655 656 657
  // 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.
658
  static bool _isUnicodeDirectionality(int value) {
659 660 661
    return value == 0x200F || value == 0x200E;
  }

662
  /// Returns the closest offset after `offset` at which the input cursor can be
663
  /// positioned.
664 665
  int? getOffsetAfter(int offset) {
    final int? nextCodeUnit = _text!.codeUnitAt(offset);
666 667
    if (nextCodeUnit == null)
      return null;
668
    // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
669 670 671
    return _isUtf16Surrogate(nextCodeUnit) ? offset + 2 : offset + 1;
  }

672
  /// Returns the closest offset before `offset` at which the input cursor can
673
  /// be positioned.
674 675
  int? getOffsetBefore(int offset) {
    final int? prevCodeUnit = _text!.codeUnitAt(offset - 1);
676 677
    if (prevCodeUnit == null)
      return null;
678
    // TODO(goderbauer): doesn't handle extended grapheme clusters with more than one Unicode scalar value (https://github.com/flutter/flutter/issues/13404).
679 680 681
    return _isUtf16Surrogate(prevCodeUnit) ? offset - 2 : offset - 1;
  }

682 683 684
  // Unicode value for a zero width joiner character.
  static const int _zwjUtf16 = 0x200d;

685
  // Get the Rect of the cursor (in logical pixels) based off the near edge
686
  // of the character upstream from the given string offset.
687 688 689
  Rect? _getRectFromUpstream(int offset, Rect caretPrototype) {
    final String flattenedText = _text!.toPlainText(includePlaceholders: false);
    final int? prevCodeUnit = _text!.codeUnitAt(max(0, offset - 1));
690 691
    if (prevCodeUnit == null)
      return null;
692

693 694 695
    // If the upstream character is a newline, cursor is at start of next line
    const int NEWLINE_CODE_UNIT = 10;

696
    // Check for multi-code-unit glyphs such as emojis or zero width joiner.
697
    final bool needsSearch = _isUtf16Surrogate(prevCodeUnit) || _text!.codeUnitAt(offset) == _zwjUtf16 || _isUnicodeDirectionality(prevCodeUnit);
698 699
    int graphemeClusterLength = needsSearch ? 2 : 1;
    List<TextBox> boxes = <TextBox>[];
700
    while (boxes.isEmpty) {
701
      final int prevRuneOffset = offset - graphemeClusterLength;
702 703
      // Use BoxHeightStyle.strut to ensure that the caret's height fits within
      // the line's height and is consistent throughout the line.
704
      boxes = _paragraph!.getBoxesForRange(prevRuneOffset, offset, boxHeightStyle: ui.BoxHeightStyle.strut);
705 706 707 708
      // 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.
709
        if (!needsSearch && prevCodeUnit == NEWLINE_CODE_UNIT) {
710
          break; // Only perform one iteration if no search is required.
711 712
        }
        if (prevRuneOffset < -flattenedText.length) {
713
          break; // Stop iterating when beyond the max length of the text.
714
        }
715 716 717 718 719 720 721
        // 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;
      }
722
      final TextBox box = boxes.first;
723 724

      if (prevCodeUnit == NEWLINE_CODE_UNIT) {
725
        return Rect.fromLTRB(_emptyOffset.dx, box.bottom, _emptyOffset.dx, box.bottom + box.bottom - box.top);
726 727
      }

728 729
      final double caretEnd = box.end;
      final double dx = box.direction == TextDirection.rtl ? caretEnd - caretPrototype.width : caretEnd;
730
      return Rect.fromLTRB(min(dx, _paragraph!.width), box.top, min(dx, _paragraph!.width), box.bottom);
731 732
    }
    return null;
733 734
  }

735
  // Get the Rect of the cursor (in logical pixels) based off the near edge
736
  // of the character downstream from the given string offset.
737 738
  Rect? _getRectFromDownstream(int offset, Rect caretPrototype) {
    final String flattenedText = _text!.toPlainText(includePlaceholders: false);
739
    // We cap the offset at the final index of the _text.
740
    final int? nextCodeUnit = _text!.codeUnitAt(min(offset, flattenedText.length - 1));
741 742
    if (nextCodeUnit == null)
      return null;
743
    // Check for multi-code-unit glyphs such as emojis or zero width joiner
744
    final bool needsSearch = _isUtf16Surrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16 || _isUnicodeDirectionality(nextCodeUnit);
745 746
    int graphemeClusterLength = needsSearch ? 2 : 1;
    List<TextBox> boxes = <TextBox>[];
747
    while (boxes.isEmpty) {
748
      final int nextRuneOffset = offset + graphemeClusterLength;
749 750
      // Use BoxHeightStyle.strut to ensure that the caret's height fits within
      // the line's height and is consistent throughout the line.
751
      boxes = _paragraph!.getBoxesForRange(offset, nextRuneOffset, boxHeightStyle: ui.BoxHeightStyle.strut);
752 753 754 755
      // 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.
756
        if (!needsSearch) {
757
          break; // Only perform one iteration if no search is required.
758 759
        }
        if (nextRuneOffset >= flattenedText.length << 1) {
760
          break; // Stop iterating when beyond the max length of the text.
761
        }
762 763 764 765 766 767 768
        // 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;
      }
769
      final TextBox box = boxes.last;
770 771
      final double caretStart = box.start;
      final double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
772
      return Rect.fromLTRB(min(dx, _paragraph!.width), box.top, min(dx, _paragraph!.width), box.bottom);
773 774
    }
    return null;
775 776
  }

777
  Offset get _emptyOffset {
Ian Hickson's avatar
Ian Hickson committed
778 779 780
    assert(!_needsLayout); // implies textDirection is non-null
    assert(textAlign != null);
    switch (textAlign) {
781 782 783
      case TextAlign.left:
        return Offset.zero;
      case TextAlign.right:
784
        return Offset(width, 0.0);
785
      case TextAlign.center:
786
        return Offset(width / 2.0, 0.0);
Ian Hickson's avatar
Ian Hickson committed
787 788 789
      case TextAlign.justify:
      case TextAlign.start:
        assert(textDirection != null);
790
        switch (textDirection!) {
Ian Hickson's avatar
Ian Hickson committed
791
          case TextDirection.rtl:
792
            return Offset(width, 0.0);
Ian Hickson's avatar
Ian Hickson committed
793 794 795 796 797
          case TextDirection.ltr:
            return Offset.zero;
        }
      case TextAlign.end:
        assert(textDirection != null);
798
        switch (textDirection!) {
Ian Hickson's avatar
Ian Hickson committed
799 800 801
          case TextDirection.rtl:
            return Offset.zero;
          case TextDirection.ltr:
802
            return Offset(width, 0.0);
Ian Hickson's avatar
Ian Hickson committed
803
        }
804 805 806
    }
  }

807
  /// Returns the offset at which to paint the caret.
808
  ///
809
  /// Valid only after [layout] has been called.
810
  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
811 812 813 814
    _computeCaretMetrics(position, caretPrototype);
    return _caretMetrics.offset;
  }

815 816 817
  /// {@template flutter.painting.textPainter.getFullHeightForCaret}
  /// Returns the strut bounded height of the glyph at the given `position`.
  /// {@endtemplate}
818 819
  ///
  /// Valid only after [layout] has been called.
820
  double? getFullHeightForCaret(TextPosition position, Rect caretPrototype) {
821 822 823 824 825 826 827
    _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.
828
  late _CaretMetrics _caretMetrics;
829 830 831

  // 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
832
  // only as necessary.
833 834
  TextPosition? _previousCaretPosition;
  Rect? _previousCaretPrototype;
835 836 837 838

  // 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) {
839
    assert(!_needsLayout);
840 841
    if (position == _previousCaretPosition && caretPrototype == _previousCaretPrototype)
      return;
842
    final int offset = position.offset;
Ian Hickson's avatar
Ian Hickson committed
843
    assert(position.affinity != null);
844
    Rect? rect;
845
    switch (position.affinity) {
846 847 848 849 850 851 852 853
      case TextAffinity.upstream: {
        rect = _getRectFromUpstream(offset, caretPrototype) ?? _getRectFromDownstream(offset, caretPrototype);
        break;
      }
      case TextAffinity.downstream: {
        rect = _getRectFromDownstream(offset, caretPrototype) ??  _getRectFromUpstream(offset, caretPrototype);
        break;
      }
854
    }
855 856 857 858
    _caretMetrics = _CaretMetrics(
      offset: rect != null ? Offset(rect.left, rect.top) : _emptyOffset,
      fullHeight: rect != null ? rect.bottom - rect.top : null,
    );
859 860 861 862

    // Cache the input parameters to prevent repeat work later.
    _previousCaretPosition = position;
    _previousCaretPrototype = caretPrototype;
863 864 865 866
  }

  /// Returns a list of rects that bound the given selection.
  ///
867 868 869 870 871
  /// 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.
  ///
872 873 874
  /// 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.
875 876 877 878 879 880 881
  ///
  /// Leading or trailing newline characters will be represented by zero-width
  /// `Textbox`es.
  ///
  /// The method only returns `TextBox`es of glyphs that are entirely enclosed by
  /// the given `selection`: a multi-code-unit glyph will be excluded if only
  /// part of its code units are in `selection`.
882 883 884 885 886
  List<TextBox> getBoxesForSelection(
    TextSelection selection, {
    ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
    ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
  }) {
887
    assert(!_needsLayout);
888 889
    assert(boxHeightStyle != null);
    assert(boxWidthStyle != null);
890
    return _paragraph!.getBoxesForRange(
891 892 893
      selection.start,
      selection.end,
      boxHeightStyle: boxHeightStyle,
894
      boxWidthStyle: boxWidthStyle,
895
    );
896 897
  }

898
  /// Returns the position within the text for the given pixel offset.
899 900
  TextPosition getPositionForOffset(Offset offset) {
    assert(!_needsLayout);
901
    return _paragraph!.getPositionForOffset(offset);
902 903
  }

904 905 906 907 908 909 910
  /// 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>.
911 912
  TextRange getWordBoundary(TextPosition position) {
    assert(!_needsLayout);
913
    return _paragraph!.getWordBoundary(position);
914 915
  }

916
  /// Returns the text range of the line at the given offset.
917
  ///
918
  /// The newline (if any) is not returned as part of the range.
919 920
  TextRange getLineBoundary(TextPosition position) {
    assert(!_needsLayout);
921
    return _paragraph!.getLineBoundary(position);
922
  }
923 924 925 926 927 928 929 930 931 932 933 934 935 936 937

  /// 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
938
  /// should be invalidated upon the next successful [layout].
939 940
  List<ui.LineMetrics> computeLineMetrics() {
    assert(!_needsLayout);
941
    return _paragraph!.computeLineMetrics();
942
  }
943
}