text_painter.dart 23.2 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:math' show min, max;
6
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle;
7

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

12
import 'basic_types.dart';
13
import 'strut_style.dart';
14
import 'text_span.dart';
15

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

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

66
  ui.Paragraph _paragraph;
67
  bool _needsLayout = true;
68

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

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

Ian Hickson's avatar
Ian Hickson committed
103 104 105 106 107 108 109 110 111
  /// 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]
112
  /// context, the English phrase will be on the right and the Hebrew phrase on
Ian Hickson's avatar
Ian Hickson committed
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
  /// its left.
  ///
  /// After this is set, you must call [layout] before the next call to [paint].
  ///
  /// This and [text] must be non-null before you call [layout].
  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;
  set textDirection(TextDirection value) {
    if (_textDirection == value)
      return;
    _textDirection = value;
    _paragraph = null;
    _layoutTemplate = null; // Shouldn't really matter, but for strict correctness...
    _needsLayout = true;
  }

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

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

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

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

204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
  /// {@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;
    _paragraph = null;
    _needsLayout = true;
  }


227
  ui.Paragraph _layoutTemplate;
228

229
  ui.ParagraphStyle _createParagraphStyle([ TextDirection defaultTextDirection ]) {
Ian Hickson's avatar
Ian Hickson committed
230 231 232 233
    // 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.');
234 235
    return _text.style?.getParagraphStyle(
      textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
236
      textDirection: textDirection ?? defaultTextDirection,
237 238 239
      textScaleFactor: textScaleFactor,
      maxLines: _maxLines,
      ellipsis: _ellipsis,
240
      locale: _locale,
241
      strutStyle: _strutStyle,
242
    ) ?? ui.ParagraphStyle(
243
      textAlign: textAlign,
Ian Hickson's avatar
Ian Hickson committed
244
      textDirection: textDirection ?? defaultTextDirection,
245 246
      maxLines: maxLines,
      ellipsis: ellipsis,
247
      locale: locale,
248 249 250
    );
  }

251
  /// The height of a space in [text] in logical pixels.
252
  ///
253 254
  /// 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
255
  /// relative a typical line of text.
256 257
  ///
  /// Obtaining this value does not require calling [layout].
Ian Hickson's avatar
Ian Hickson committed
258 259 260 261 262
  ///
  /// 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).
263 264
  double get preferredLineHeight {
    if (_layoutTemplate == null) {
265
      final ui.ParagraphBuilder builder = ui.ParagraphBuilder(
Ian Hickson's avatar
Ian Hickson committed
266
        _createParagraphStyle(TextDirection.rtl),
267
      ); // direction doesn't matter, text is just a space
Ian Hickson's avatar
Ian Hickson committed
268
      if (text?.style != null)
269
        builder.pushStyle(text.style.getTextStyle(textScaleFactor: textScaleFactor));
270
      builder.addText(' ');
271
      _layoutTemplate = builder.build()
272
        ..layout(const ui.ParagraphConstraints(width: double.infinity));
273 274 275
    }
    return _layoutTemplate.height;
  }
276

277 278 279 280 281 282 283 284 285 286 287
  // 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();
  }

288 289
  /// The width at which decreasing the width of the text would prevent it from
  /// painting itself completely within its bounds.
290
  ///
291
  /// Valid only after [layout] has been called.
292
  double get minIntrinsicWidth {
293 294 295 296
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.minIntrinsicWidth);
  }

Florian Loitsch's avatar
Florian Loitsch committed
297
  /// The width at which increasing the width of the text no longer decreases the height.
298
  ///
299
  /// Valid only after [layout] has been called.
300
  double get maxIntrinsicWidth {
301 302 303 304
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.maxIntrinsicWidth);
  }

305 306
  /// The horizontal space required to paint this text.
  ///
307
  /// Valid only after [layout] has been called.
308 309 310 311 312
  double get width {
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.width);
  }

313 314
  /// The vertical space required to paint this text.
  ///
315
  /// Valid only after [layout] has been called.
316 317 318 319 320
  double get height {
    assert(!_needsLayout);
    return _applyFloatingPointHack(_paragraph.height);
  }

321 322
  /// The amount of space required to paint this text.
  ///
323
  /// Valid only after [layout] has been called.
324
  Size get size {
325
    assert(!_needsLayout);
326
    return Size(width, height);
327 328
  }

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

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

361 362
  double _lastMinWidth;
  double _lastMaxWidth;
363

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

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

418 419
  // 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'.
420 421 422 423
  bool _isUtf16Surrogate(int value) {
    return value & 0xF800 == 0xD800;
  }

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

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

444 445 446
  // Unicode value for a zero width joiner character.
  static const int _zwjUtf16 = 0x200d;

447 448
  // Get the Offset of the cursor (in logical pixels) based off the near edge
  // of the character upstream from the given string offset.
449 450
  // TODO(garyq): Use actual extended grapheme cluster length instead of
  // an increasing cluster length amount to achieve deterministic performance.
451
  Offset _getOffsetFromUpstream(int offset, Rect caretPrototype) {
452
    final String flattenedText = _text.toPlainText();
453
    final int prevCodeUnit = _text.codeUnitAt(max(0, offset - 1));
454 455
    if (prevCodeUnit == null)
      return null;
456

457 458 459 460
    // Check for multi-code-unit glyphs such as emojis or zero width joiner
    final bool needsSearch = _isUtf16Surrogate(prevCodeUnit) || _text.codeUnitAt(offset) == _zwjUtf16;
    int graphemeClusterLength = needsSearch ? 2 : 1;
    List<TextBox> boxes = <TextBox>[];
461
    while (boxes.isEmpty && flattenedText != null) {
462 463 464 465 466 467 468 469
      final int prevRuneOffset = offset - graphemeClusterLength;
      boxes = _paragraph.getBoxesForRange(prevRuneOffset, offset);
      // 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.
        if (!needsSearch)
          break; // Only perform one iteration if no search is required.
470
        if (prevRuneOffset < -flattenedText.length)
471 472 473 474 475 476 477 478
          break; // Stop iterating when beyond the max length of the text.
        // 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;
      }
479
      final TextBox box = boxes.first;
480 481 482 483 484 485 486

      // 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) {
        return Offset(_emptyOffset.dx, box.bottom);
      }

487 488 489 490 491
      final double caretEnd = box.end;
      final double dx = box.direction == TextDirection.rtl ? caretEnd - caretPrototype.width : caretEnd;
      return Offset(dx, box.top);
    }
    return null;
492 493
  }

494 495
  // Get the Offset of the cursor (in logical pixels) based off the near edge
  // of the character downstream from the given string offset.
496 497
  // TODO(garyq): Use actual extended grapheme cluster length instead of
  // an increasing cluster length amount to achieve deterministic performance.
498
  Offset _getOffsetFromDownstream(int offset, Rect caretPrototype) {
499
    final String flattenedText = _text.toPlainText();
500
    // We cap the offset at the final index of the _text.
501
    final int nextCodeUnit = _text.codeUnitAt(min(offset, flattenedText == null ? 0 : flattenedText.length - 1));
502 503
    if (nextCodeUnit == null)
      return null;
504 505 506 507
    // Check for multi-code-unit glyphs such as emojis or zero width joiner
    final bool needsSearch = _isUtf16Surrogate(nextCodeUnit) || nextCodeUnit == _zwjUtf16;
    int graphemeClusterLength = needsSearch ? 2 : 1;
    List<TextBox> boxes = <TextBox>[];
508
    while (boxes.isEmpty && flattenedText != null) {
509 510 511 512 513 514 515 516
      final int nextRuneOffset = offset + graphemeClusterLength;
      boxes = _paragraph.getBoxesForRange(offset, nextRuneOffset);
      // 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.
        if (!needsSearch)
          break; // Only perform one iteration if no search is required.
517
        if (nextRuneOffset >= flattenedText.length << 1)
518 519 520 521 522 523 524 525
          break; // Stop iterating when beyond the max length of the text.
        // 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;
      }
526
      final TextBox box = boxes.last;
527 528 529 530 531
      final double caretStart = box.start;
      final double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
      return Offset(dx, box.top);
    }
    return null;
532 533
  }

534
  Offset get _emptyOffset {
Ian Hickson's avatar
Ian Hickson committed
535 536 537
    assert(!_needsLayout); // implies textDirection is non-null
    assert(textAlign != null);
    switch (textAlign) {
538 539 540
      case TextAlign.left:
        return Offset.zero;
      case TextAlign.right:
541
        return Offset(width, 0.0);
542
      case TextAlign.center:
543
        return Offset(width / 2.0, 0.0);
Ian Hickson's avatar
Ian Hickson committed
544 545 546 547 548
      case TextAlign.justify:
      case TextAlign.start:
        assert(textDirection != null);
        switch (textDirection) {
          case TextDirection.rtl:
549
            return Offset(width, 0.0);
Ian Hickson's avatar
Ian Hickson committed
550 551 552 553 554 555 556 557 558 559
          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:
560
            return Offset(width, 0.0);
Ian Hickson's avatar
Ian Hickson committed
561 562
        }
        return null;
563 564 565 566
    }
    return null;
  }

567
  /// Returns the offset at which to paint the caret.
568
  ///
569
  /// Valid only after [layout] has been called.
570 571
  Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
    assert(!_needsLayout);
572
    final int offset = position.offset;
Ian Hickson's avatar
Ian Hickson committed
573
    assert(position.affinity != null);
574 575 576 577
    switch (position.affinity) {
      case TextAffinity.upstream:
        return _getOffsetFromUpstream(offset, caretPrototype)
            ?? _getOffsetFromDownstream(offset, caretPrototype)
578
            ?? _emptyOffset;
579 580 581
      case TextAffinity.downstream:
        return _getOffsetFromDownstream(offset, caretPrototype)
            ?? _getOffsetFromUpstream(offset, caretPrototype)
582
            ?? _emptyOffset;
583
    }
pq's avatar
pq committed
584
    return null;
585 586 587 588 589 590 591
  }

  /// Returns a list of rects that bound the given selection.
  ///
  /// A given selection might have more than one rect if this text painter
  /// contains bidirectional text because logically contiguous text might not be
  /// visually contiguous.
Ian Hickson's avatar
Ian Hickson committed
592
  List<TextBox> getBoxesForSelection(TextSelection selection) {
593 594 595 596
    assert(!_needsLayout);
    return _paragraph.getBoxesForRange(selection.start, selection.end);
  }

597
  /// Returns the position within the text for the given pixel offset.
598 599 600 601 602
  TextPosition getPositionForOffset(Offset offset) {
    assert(!_needsLayout);
    return _paragraph.getPositionForOffset(offset);
  }

603 604 605 606 607 608 609
  /// 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>.
610 611
  TextRange getWordBoundary(TextPosition position) {
    assert(!_needsLayout);
612
    final List<int> indices = _paragraph.getWordBoundary(position.offset);
613
    return TextRange(start: indices[0], end: indices[1]);
614
  }
615
}