LCOV - code coverage report
Current view: top level - base - mongol_text_painter.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 214 225 95.1 %
Date: 2021-08-02 17:55:49 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright 2014 The Flutter Authors.
       2             : // Copyright 2020 Suragch.
       3             : // All rights reserved.
       4             : // Use of this source code is governed by a BSD-style license that can be
       5             : // found in the LICENSE file.
       6             : 
       7             : import 'dart:math';
       8             : import 'dart:ui' as ui show ParagraphStyle;
       9             : 
      10             : import 'package:flutter/widgets.dart';
      11             : import 'package:mongol/src/base/mongol_paragraph.dart';
      12             : 
      13             : import 'mongol_text_align.dart';
      14             : 
      15             : // The default font size if none is specified. This should be kept in
      16             : // sync with the default values in text_style.dart, as well as the
      17             : // defaults set in the engine (eg, LibTxt's text_style.h, paragraph_style.h).
      18             : const double _kDefaultFontSize = 14.0;
      19             : 
      20             : /// This is used to cache and pass the computed metrics regarding the
      21             : /// caret's size and position. This is preferred due to the expensive
      22             : /// nature of the calculation.
      23             : class _CaretMetrics {
      24           4 :   const _CaretMetrics({required this.offset, this.fullWidth});
      25             : 
      26             :   /// The offset of the top left corner of the caret from the top left
      27             :   /// corner of the paragraph.
      28             :   final Offset offset;
      29             : 
      30             :   /// The full width of the glyph at the caret position.
      31             :   ///
      32             :   /// Orientation is a vertical paragraph with horizontal caret.
      33             :   final double? fullWidth;
      34             : }
      35             : 
      36             : /// A convenience method for converting MongolTextAlign to TextAlign
      37           8 : MongolTextAlign? mapHorizontalToMongolTextAlign(TextAlign? textAlign) {
      38             :   if (textAlign == null) return null;
      39             :   switch (textAlign) {
      40           1 :     case TextAlign.left:
      41           1 :     case TextAlign.start:
      42             :       return MongolTextAlign.top;
      43           1 :     case TextAlign.right:
      44           1 :     case TextAlign.end:
      45             :       return MongolTextAlign.bottom;
      46           1 :     case TextAlign.center:
      47             :       return MongolTextAlign.center;
      48           1 :     case TextAlign.justify:
      49             :       return MongolTextAlign.justify;
      50             :   }
      51             : }
      52             : 
      53             : /// An object that paints a Mongolian [TextSpan] tree into a [Canvas].
      54             : ///
      55             : /// To use a [MongolTextPainter], follow these steps:
      56             : ///
      57             : /// 1. Create a [TextSpan] tree and pass it to the [MongolTextPainter]
      58             : ///    constructor.
      59             : ///
      60             : /// 2. Call [layout] to prepare the paragraph.
      61             : ///
      62             : /// 3. Call [paint] as often as desired to paint the paragraph.
      63             : ///
      64             : /// If the width of the area into which the text is being painted
      65             : /// changes, return to step 2. If the text to be painted changes,
      66             : /// return to step 1.
      67             : ///
      68             : /// The default text style is white. To change the color of the text,
      69             : /// pass a [TextStyle] object to the [TextSpan] in `text`.
      70             : class MongolTextPainter {
      71             :   /// Creates a text painter that paints the given text.
      72             :   ///
      73             :   /// The `text` argument is optional but [text] must be non-null before
      74             :   /// calling [layout].
      75          11 :   MongolTextPainter({
      76             :     TextSpan? text,
      77             :     MongolTextAlign textAlign = MongolTextAlign.top,
      78             :     double textScaleFactor = 1.0,
      79             :     int? maxLines,
      80             :     String? ellipsis,
      81          11 :   })  : assert(text == null || text.debugAssertIsValid()),
      82           1 :         assert(maxLines == null || maxLines > 0),
      83             :         _text = text,
      84             :         _textAlign = textAlign,
      85             :         _textScaleFactor = textScaleFactor,
      86             :         _maxLines = maxLines,
      87             :         _ellipsis = ellipsis;
      88             : 
      89             :   MongolParagraph? _paragraph;
      90             :   bool _needsLayout = true;
      91             : 
      92             :   /// Marks this text painter's layout information as dirty and removes cached
      93             :   /// information.
      94             :   ///
      95             :   /// Uses this method to notify text painter to relayout in the case of
      96             :   /// layout changes in engine. In most cases, updating text painter properties
      97             :   /// in framework will automatically invoke this method.
      98           9 :   void markNeedsLayout() {
      99           9 :     _paragraph = null;
     100           9 :     _needsLayout = true;
     101           9 :     _previousCaretPosition = null;
     102           9 :     _previousCaretPrototype = null;
     103             :   }
     104             : 
     105             :   /// The (potentially styled) text to paint.
     106             :   ///
     107             :   /// After this is set, you must call [layout] before the next call to [paint].
     108             :   /// This must be non-null before you call [layout].
     109             :   ///
     110             :   /// The [TextSpan] this provides is in the form of a tree that may contain
     111             :   /// multiple instances of [TextSpan]s. To obtain a plain text
     112             :   /// representation of the contents of this [TextPainter], use
     113             :   /// [TextSpan.toPlainText] to get the full contents of all nodes in the tree.
     114             :   /// [TextSpan.text] will only provide the contents of the first node in the
     115             :   /// tree.
     116          22 :   TextSpan? get text => _text;
     117             :   TextSpan? _text;
     118           8 :   set text(TextSpan? value) {
     119           8 :     assert(value == null || value.debugAssertIsValid());
     120          16 :     if (_text == value) return;
     121          32 :     if (_text?.style != value?.style) {
     122           7 :       _layoutTemplate = null;
     123             :     }
     124           8 :     _text = value;
     125           8 :     markNeedsLayout();
     126             :   }
     127             : 
     128             :   /// How the text should be aligned vertically.
     129             :   ///
     130             :   /// After this is set, you must call [layout] before the next call to [paint].
     131             :   ///
     132             :   /// The [textAlign] property defaults to [MongolTextAlign.top].
     133          18 :   MongolTextAlign get textAlign => _textAlign;
     134             :   MongolTextAlign _textAlign;
     135           4 :   set textAlign(MongolTextAlign value) {
     136           8 :     if (_textAlign == value) {
     137             :       return;
     138             :     }
     139           4 :     _textAlign = value;
     140           4 :     markNeedsLayout();
     141             :   }
     142             : 
     143             :   /// The number of font pixels for each logical pixel.
     144             :   ///
     145             :   /// For example, if the text scale factor is 1.5, text will be 50% larger than
     146             :   /// the specified font size.
     147             :   ///
     148             :   /// After this is set, you must call [layout] before the next call to [paint].
     149          22 :   double get textScaleFactor => _textScaleFactor;
     150             :   double _textScaleFactor;
     151           3 :   set textScaleFactor(double value) {
     152           6 :     if (_textScaleFactor == value) return;
     153           3 :     _textScaleFactor = value;
     154           3 :     markNeedsLayout();
     155           3 :     _layoutTemplate = null;
     156             :   }
     157             : 
     158             :   /// The string used to ellipsize overflowing text. Setting this to a non-empty
     159             :   /// string will cause this string to be substituted for the remaining text
     160             :   /// if the text can not fit within the specified maximum height.
     161             :   ///
     162             :   /// Specifically, the ellipsis is applied to the last line before the line
     163             :   /// truncated by [maxLines], if [maxLines] is non-null and that line overflows
     164             :   /// the height constraint, or to the first line that is taller than the height
     165             :   /// constraint, if [maxLines] is null. The height constraint is the `maxHeight`
     166             :   /// passed to [layout].
     167             :   ///
     168             :   /// After this is set, you must call [layout] before the next call to [paint].
     169             :   ///
     170             :   /// The higher layers of the system, such as the [MongolText] widget, represent
     171             :   /// overflow effects using the [TextOverflow] enum. The
     172             :   /// [TextOverflow.ellipsis] value corresponds to setting this property to
     173             :   /// U+2026 HORIZONTAL ELLIPSIS (…).
     174          22 :   String? get ellipsis => _ellipsis;
     175             :   String? _ellipsis;
     176           1 :   set ellipsis(String? value) {
     177           0 :     assert(value == null || value.isNotEmpty);
     178           2 :     if (_ellipsis == value) {
     179             :       return;
     180             :     }
     181           0 :     _ellipsis = value;
     182           0 :     markNeedsLayout();
     183             :   }
     184             : 
     185             :   /// An optional maximum number of lines for the text to span, wrapping if
     186             :   /// necessary.
     187             :   ///
     188             :   /// If the text exceeds the given number of lines, it is truncated such that
     189             :   /// subsequent lines are dropped.
     190             :   ///
     191             :   /// After this is set, you must call [layout] before the next call to [paint].
     192          22 :   int? get maxLines => _maxLines;
     193             :   int? _maxLines;
     194             : 
     195             :   /// The value may be null. If it is not null, then it must be greater than zero.
     196           1 :   set maxLines(int? value) {
     197           1 :     assert(value == null || value > 0);
     198           2 :     if (_maxLines == value) {
     199             :       return;
     200             :     }
     201           1 :     _maxLines = value;
     202           1 :     markNeedsLayout();
     203             :   }
     204             : 
     205             :   MongolParagraph? _layoutTemplate;
     206             : 
     207          11 :   ui.ParagraphStyle _createParagraphStyle() {
     208             :     // textAlign should always be `left` because this is the style for
     209             :     // a single text run. MongolTextAlign is handled elsewhere.
     210          33 :     return _text!.style?.getParagraphStyle(
     211             :           textAlign: TextAlign.left,
     212             :           textDirection: TextDirection.ltr,
     213          11 :           textScaleFactor: textScaleFactor,
     214          11 :           maxLines: maxLines,
     215          11 :           ellipsis: ellipsis,
     216             :           locale: null,
     217             :           strutStyle: null,
     218             :         ) ??
     219           3 :         ui.ParagraphStyle(
     220             :           textAlign: TextAlign.left,
     221             :           textDirection: TextDirection.ltr,
     222             :           // Use the default font size to multiply by as RichText does not
     223             :           // perform inheriting [TextStyle]s and would otherwise
     224             :           // fail to apply textScaleFactor.
     225           6 :           fontSize: _kDefaultFontSize * textScaleFactor,
     226           3 :           maxLines: maxLines,
     227           3 :           ellipsis: ellipsis,
     228             :           locale: null,
     229             :         );
     230             :   }
     231             : 
     232             :   /// The width of a space in [text] in logical pixels.
     233             :   ///
     234             :   /// (This is in vertical orientation. In other words, it is the height
     235             :   /// of a space in horizontal orientation.)
     236             :   ///
     237             :   /// Not every line of text in [text] will have this width, but this width
     238             :   /// is "typical" for text in [text] and useful for sizing other objects
     239             :   /// relative a typical line of text.
     240             :   ///
     241             :   /// Obtaining this value does not require calling [layout].
     242             :   ///
     243             :   /// The style of the [text] property is used to determine the font settings
     244             :   /// that contribute to the [preferredLineWidth]. If [text] is null or if it
     245             :   /// specifies no styles, the default [TextStyle] values are used (a 10 pixel
     246             :   /// sans-serif font).
     247           4 :   double get preferredLineWidth {
     248           4 :     if (_layoutTemplate == null) {
     249           8 :       final builder = MongolParagraphBuilder(_createParagraphStyle());
     250           8 :       if (text?.style != null) {
     251          12 :         builder.pushStyle(text!.style!);
     252             :       }
     253           4 :       builder.addText(' ');
     254           8 :       _layoutTemplate = builder.build()
     255           4 :         ..layout(const MongolParagraphConstraints(height: double.infinity));
     256             :     }
     257           8 :     return _layoutTemplate!.width;
     258             :   }
     259             : 
     260          11 :   double _applyFloatingPointHack(double layoutValue) {
     261          11 :     return layoutValue.ceilToDouble();
     262             :   }
     263             : 
     264             :   /// The height at which decreasing the height of the text would prevent it from
     265             :   /// painting itself completely within its bounds.
     266             :   ///
     267             :   /// Valid only after [layout] has been called.
     268           7 :   double get minIntrinsicHeight {
     269           7 :     assert(!_needsLayout);
     270          21 :     return _applyFloatingPointHack(_paragraph!.minIntrinsicHeight);
     271             :   }
     272             : 
     273             :   /// The height at which increasing the height of the text no longer decreases
     274             :   /// the width.
     275             :   ///
     276             :   /// Valid only after [layout] has been called.
     277          10 :   double get maxIntrinsicHeight {
     278          10 :     assert(!_needsLayout);
     279          30 :     return _applyFloatingPointHack(_paragraph!.maxIntrinsicHeight);
     280             :   }
     281             : 
     282             :   /// The horizontal space required to paint this text.
     283             :   ///
     284             :   /// Valid only after [layout] has been called.
     285          11 :   double get width {
     286          11 :     assert(!_needsLayout);
     287          33 :     return _applyFloatingPointHack(_paragraph!.width);
     288             :   }
     289             : 
     290             :   /// The vertical space required to paint this text.
     291             :   ///
     292             :   /// Valid only after [layout] has been called.
     293          11 :   double get height {
     294          11 :     assert(!_needsLayout);
     295          33 :     return _applyFloatingPointHack(_paragraph!.height);
     296             :   }
     297             : 
     298             :   /// The amount of space required to paint this text.
     299             :   ///
     300             :   /// Valid only after [layout] has been called.
     301          11 :   Size get size {
     302          11 :     assert(!_needsLayout);
     303          33 :     return Size(width, height);
     304             :   }
     305             : 
     306             :   /// Even though the text is rotated, it is still useful to have a baseline
     307             :   /// along which to layout objects. (For example in the MongolInputDecorator.)
     308             :   ///
     309             :   /// Valid only after [layout] has been called.
     310           2 :   double computeDistanceToActualBaseline(TextBaseline baseline) {
     311           2 :     assert(!_needsLayout);
     312             :     switch (baseline) {
     313           2 :       case TextBaseline.alphabetic:
     314           4 :         return _paragraph!.alphabeticBaseline;
     315           0 :       case TextBaseline.ideographic:
     316           0 :         return _paragraph!.ideographicBaseline;
     317             :     }
     318             :   }
     319             : 
     320             :   /// Whether any text was truncated or ellipsized.
     321             :   ///
     322             :   /// If [maxLines] is not null, this is true if there were more lines to be
     323             :   /// drawn than the given [maxLines], and thus at least one line was omitted in
     324             :   /// the output; otherwise it is false.
     325             :   ///
     326             :   /// If [maxLines] is null, this is true if [ellipsis] is not the empty string
     327             :   /// and there was a line that overflowed the `maxHeight` argument passed to
     328             :   /// [layout]; otherwise it is false.
     329             :   ///
     330             :   /// Valid only after [layout] has been called.
     331           8 :   bool get didExceedMaxLines {
     332           8 :     assert(!_needsLayout);
     333          16 :     return _paragraph!.didExceedMaxLines;
     334             :   }
     335             : 
     336             :   double? _lastMinHeight;
     337             :   double? _lastMaxHeight;
     338             : 
     339             :   /// Computes the visual position of the glyphs for painting the text.
     340             :   ///
     341             :   /// The text will layout with a height that's as close to its max intrinsic
     342             :   /// height as possible while still being greater than or equal to `minHeight`
     343             :   /// and less than or equal to `maxHeight`.
     344             :   ///
     345             :   /// The [text] property must be non-null before this is called.
     346          11 :   void layout({double minHeight = 0.0, double maxHeight = double.infinity}) {
     347          11 :     assert(text != null);
     348          11 :     if (!_needsLayout &&
     349          22 :         minHeight == _lastMinHeight &&
     350          22 :         maxHeight == _lastMaxHeight) return;
     351          11 :     _needsLayout = false;
     352          11 :     if (_paragraph == null) {
     353          11 :       final builder = MongolParagraphBuilder(
     354          11 :         _createParagraphStyle(),
     355          11 :         textAlign: _textAlign,
     356          11 :         textScaleFactor: _textScaleFactor,
     357          11 :         maxLines: _maxLines,
     358          11 :         ellipsis: _ellipsis,
     359             :       );
     360          22 :       _addStyleToText(builder, _text!);
     361          22 :       _paragraph = builder.build();
     362             :     }
     363          11 :     _lastMinHeight = minHeight;
     364          11 :     _lastMaxHeight = maxHeight;
     365             :     // A change in layout invalidates the cached caret metrics as well.
     366          11 :     _previousCaretPosition = null;
     367          11 :     _previousCaretPrototype = null;
     368          33 :     _paragraph!.layout(MongolParagraphConstraints(height: maxHeight));
     369          11 :     if (minHeight != maxHeight) {
     370          20 :       final newHeight = maxIntrinsicHeight.clamp(minHeight, maxHeight);
     371          20 :       if (newHeight != height) {
     372          30 :         _paragraph!.layout(MongolParagraphConstraints(height: newHeight));
     373             :       }
     374             :     }
     375             :   }
     376             : 
     377          11 :   void _addStyleToText(
     378             :     MongolParagraphBuilder builder,
     379             :     InlineSpan inlineSpan,
     380             :   ) {
     381          11 :     if (inlineSpan is! TextSpan) {
     382           0 :       throw UnimplementedError(
     383             :           'Inline span support has not yet been implemented for MongolTextPainter');
     384             :     }
     385             :     final textSpan = inlineSpan;
     386          11 :     final style = textSpan.style;
     387          11 :     final text = textSpan.text;
     388          11 :     final children = textSpan.children;
     389             :     final hasStyle = style != null;
     390          11 :     if (hasStyle) builder.pushStyle(style!);
     391          11 :     if (text != null) builder.addText(text);
     392             :     if (children != null) {
     393          10 :       for (final child in children) {
     394           5 :         _addStyleToText(builder, child);
     395             :       }
     396             :     }
     397          11 :     if (hasStyle) builder.pop();
     398             :   }
     399             : 
     400             :   /// Paints the text onto the given canvas at the given offset.
     401             :   ///
     402             :   /// Valid only after [layout] has been called.
     403             :   ///
     404             :   /// If you cannot see the text being painted, check that your text color does
     405             :   /// not conflict with the background on which you are drawing. The default
     406             :   /// text color is white (to contrast with the default black background color),
     407             :   /// so if you are writing an application with a white background, the text
     408             :   /// will not be visible by default.
     409             :   ///
     410             :   /// To set the text style, specify a [TextStyle] when creating the [TextSpan]
     411             :   /// that you pass to the [MongolTextPainter] constructor or to the [text]
     412             :   /// property.
     413          11 :   void paint(Canvas canvas, Offset offset) {
     414          11 :     assert(() {
     415          11 :       if (_needsLayout) {
     416           1 :         throw FlutterError(
     417             :             'TextPainter.paint called when text geometry was not yet calculated.\n'
     418             :             'Please call layout() before paint() to position the text before painting it.');
     419             :       }
     420             :       return true;
     421          11 :     }());
     422          20 :     _paragraph!.draw(canvas, offset);
     423             :   }
     424             : 
     425             :   // Returns true iff the given value is a valid UTF-16 surrogate. The value
     426             :   // must be a UTF-16 code unit, meaning it must be in the range 0x0000-0xFFFF.
     427             :   //
     428             :   // See also:
     429             :   //   * https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
     430           4 :   static bool _isUtf16Surrogate(int value) {
     431           8 :     return value & 0xF800 == 0xD800;
     432             :   }
     433             : 
     434             :   // Checks if the glyph is either [Unicode.RLM] or [Unicode.LRM]. These values take
     435             :   // up zero space and do not have valid bounding boxes around them.
     436             :   //
     437             :   // We do not directly use the [Unicode] constants since they are strings.
     438           4 :   static bool _isUnicodeDirectionality(int value) {
     439           8 :     return value == 0x200F || value == 0x200E;
     440             :   }
     441             : 
     442             :   /// Returns the closest offset after `offset` at which the input cursor can be
     443             :   /// positioned.
     444           3 :   int? getOffsetAfter(int offset) {
     445           6 :     final nextCodeUnit = _text!.codeUnitAt(offset);
     446             :     if (nextCodeUnit == null) return null;
     447           8 :     return _isUtf16Surrogate(nextCodeUnit) ? offset + 2 : offset + 1;
     448             :   }
     449             : 
     450             :   /// Returns the closest offset before `offset` at which the input cursor can
     451             :   /// be positioned.
     452           3 :   int? getOffsetBefore(int offset) {
     453           9 :     final prevCodeUnit = _text!.codeUnitAt(offset - 1);
     454             :     if (prevCodeUnit == null) return null;
     455           8 :     return _isUtf16Surrogate(prevCodeUnit) ? offset - 2 : offset - 1;
     456             :   }
     457             : 
     458             :   // Unicode value for a zero width joiner character.
     459             :   static const int _zwjUtf16 = 0x200d;
     460             : 
     461             :   // Get the Rect of the cursor (in logical pixels) based off the near edge
     462             :   // of the character upstream from the given string offset.
     463           4 :   Rect? _getRectFromUpstream(int offset, Rect caretPrototype) {
     464           8 :     final flattenedText = _text!.toPlainText(includePlaceholders: false);
     465          16 :     final prevCodeUnit = _text!.codeUnitAt(max(0, offset - 1));
     466             :     if (prevCodeUnit == null) {
     467             :       return null;
     468             :     }
     469             : 
     470             :     // Check for multi-code-unit glyphs such as emojis or zero width joiner.
     471           4 :     final needsSearch = _isUtf16Surrogate(prevCodeUnit) ||
     472          12 :         _text!.codeUnitAt(offset) == _zwjUtf16 ||
     473           4 :         _isUnicodeDirectionality(prevCodeUnit);
     474             :     var graphemeClusterLength = needsSearch ? 2 : 1;
     475           4 :     var boxes = <Rect>[];
     476           4 :     while (boxes.isEmpty) {
     477           4 :       final prevRuneOffset = offset - graphemeClusterLength;
     478           8 :       boxes = _paragraph!.getBoxesForRange(prevRuneOffset, offset);
     479             :       // When the range does not include a full cluster, no boxes will be returned.
     480           4 :       if (boxes.isEmpty) {
     481             :         // When we are at the beginning of the line, a non-surrogate position will
     482             :         // return empty boxes. We break and try from downstream instead.
     483             :         if (!needsSearch) {
     484             :           break; // Only perform one iteration if no search is required.
     485             :         }
     486           6 :         if (prevRuneOffset < -flattenedText.length) {
     487             :           break; // Stop iterating when beyond the max length of the text.
     488             :         }
     489             :         // Multiply by two to log(n) time cover the entire text span. This allows
     490             :         // faster discovery of very long clusters and reduces the possibility
     491             :         // of certain large clusters taking much longer than others, which can
     492             :         // cause jank.
     493           2 :         graphemeClusterLength *= 2;
     494             :         continue;
     495             :       }
     496           4 :       final box = boxes.first;
     497             : 
     498             :       // If the upstream character is a newline, cursor is at start of next line
     499             :       const NEWLINE_CODE_UNIT = 10;
     500           4 :       if (prevCodeUnit == NEWLINE_CODE_UNIT) {
     501           4 :         return Rect.fromLTRB(box.right, _emptyOffset.dy,
     502           7 :             box.right + box.right - box.left, _emptyOffset.dy);
     503             :       }
     504             : 
     505           4 :       final dy = box.bottom;
     506          24 :       return Rect.fromLTRB(box.left, min(dy, _paragraph!.height), box.right,
     507          12 :           min(dy, _paragraph!.height));
     508             :     }
     509             :     return null;
     510             :   }
     511             : 
     512             :   // Get the Rect of the cursor (in logical pixels) based off the near edge
     513             :   // of the character downstream from the given string offset.
     514           4 :   Rect? _getRectFromDownstream(int offset, Rect caretPrototype) {
     515           8 :     final flattenedText = _text!.toPlainText(includePlaceholders: false);
     516             :     // We cap the offset at the final index of the _text.
     517             :     final nextCodeUnit =
     518          20 :         _text!.codeUnitAt(min(offset, flattenedText.length - 1));
     519             :     if (nextCodeUnit == null) return null;
     520             :     // Check for multi-code-unit glyphs such as emojis or zero width joiner
     521           4 :     final needsSearch = _isUtf16Surrogate(nextCodeUnit) ||
     522           4 :         nextCodeUnit == _zwjUtf16 ||
     523           4 :         _isUnicodeDirectionality(nextCodeUnit);
     524             :     var graphemeClusterLength = needsSearch ? 2 : 1;
     525           4 :     var boxes = <Rect>[];
     526           4 :     while (boxes.isEmpty) {
     527           4 :       final nextRuneOffset = offset + graphemeClusterLength;
     528           8 :       boxes = _paragraph!.getBoxesForRange(offset, nextRuneOffset);
     529             :       // When the range does not include a full grapheme cluster, no boxes will
     530             :       // be returned.
     531           4 :       if (boxes.isEmpty) {
     532             :         // When we are at the end of the line, a non-surrogate position will
     533             :         // return empty boxes. We break and try from upstream instead.
     534             :         if (!needsSearch) {
     535             :           break; // Only perform one iteration if no search is required.
     536             :         }
     537           6 :         if (nextRuneOffset >= flattenedText.length << 1) {
     538             :           break; // Stop iterating when beyond the max length of the text.
     539             :         }
     540             :         // Multiply by two to log(n) time cover the entire text span. This allows
     541             :         // faster discovery of very long clusters and reduces the possibility
     542             :         // of certain large clusters taking much longer than others, which can
     543             :         // cause jank.
     544           2 :         graphemeClusterLength *= 2;
     545             :         continue;
     546             :       }
     547           4 :       final box = boxes.last;
     548           4 :       final dy = box.top;
     549          24 :       return Rect.fromLTRB(box.left, min(dy, _paragraph!.height), box.right,
     550          12 :           min(dy, _paragraph!.height));
     551             :     }
     552             :     return null;
     553             :   }
     554             : 
     555           3 :   Offset get _emptyOffset {
     556           3 :     assert(!_needsLayout);
     557           3 :     switch (textAlign) {
     558           3 :       case MongolTextAlign.top:
     559           0 :       case MongolTextAlign.justify:
     560             :         return Offset.zero;
     561           0 :       case MongolTextAlign.bottom:
     562           0 :         return Offset(0.0, height);
     563           0 :       case MongolTextAlign.center:
     564           0 :         return Offset(0.0, height / 2.0);
     565             :     }
     566             :   }
     567             : 
     568             :   /// Returns the offset at which to paint the caret.
     569             :   ///
     570             :   /// Valid only after [layout] has been called.
     571           4 :   Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
     572           4 :     _computeCaretMetrics(position, caretPrototype);
     573           8 :     return _caretMetrics.offset;
     574             :   }
     575             : 
     576             :   /// Returns the strut bounded width of the glyph at the given `position`.
     577             :   ///
     578             :   /// Valid only after [layout] has been called.
     579           3 :   double? getFullWidthForCaret(TextPosition position, Rect caretPrototype) {
     580           3 :     _computeCaretMetrics(position, caretPrototype);
     581           6 :     return _caretMetrics.fullWidth;
     582             :   }
     583             : 
     584             :   // Cached caret metrics. This allows multiple invokes of [getOffsetForCaret] and
     585             :   // [getFullWidthForCaret] in a row without performing redundant and expensive
     586             :   // get rect calls to the paragraph.
     587             :   late _CaretMetrics _caretMetrics;
     588             : 
     589             :   // Holds the TextPosition and caretPrototype the last caret metrics were
     590             :   // computed with. When new values are passed in, we recompute the caret metrics,
     591             :   // only as necessary.
     592             :   TextPosition? _previousCaretPosition;
     593             :   Rect? _previousCaretPrototype;
     594             : 
     595             :   // Checks if the [position] and [caretPrototype] have changed from the cached
     596             :   // version and recomputes the metrics required to position the caret.
     597           4 :   void _computeCaretMetrics(TextPosition position, Rect caretPrototype) {
     598           4 :     assert(!_needsLayout);
     599           8 :     if (position == _previousCaretPosition &&
     600           6 :         caretPrototype == _previousCaretPrototype) {
     601             :       return;
     602             :     }
     603           4 :     final offset = position.offset;
     604             :     Rect? rect;
     605           4 :     switch (position.affinity) {
     606           4 :       case TextAffinity.upstream:
     607             :         {
     608           4 :           rect = _getRectFromUpstream(offset, caretPrototype) ??
     609           3 :               _getRectFromDownstream(offset, caretPrototype);
     610             :           break;
     611             :         }
     612           4 :       case TextAffinity.downstream:
     613             :         {
     614           4 :           rect = _getRectFromDownstream(offset, caretPrototype) ??
     615           4 :               _getRectFromUpstream(offset, caretPrototype);
     616             :           break;
     617             :         }
     618             :     }
     619           8 :     _caretMetrics = _CaretMetrics(
     620          15 :       offset: rect != null ? Offset(rect.left, rect.top) : _emptyOffset,
     621          12 :       fullWidth: rect != null ? rect.right - rect.left : null,
     622             :     );
     623             : 
     624             :     // Cache the input parameters to prevent repeat work later.
     625           4 :     _previousCaretPosition = position;
     626           4 :     _previousCaretPrototype = caretPrototype;
     627             :   }
     628             : 
     629             :   /// Returns a list of rects that bound the given selection.
     630           3 :   List<Rect> getBoxesForSelection(TextSelection selection) {
     631           3 :     assert(!_needsLayout);
     632           6 :     return _paragraph!.getBoxesForRange(
     633           3 :       selection.start,
     634           3 :       selection.end,
     635             :     );
     636             :   }
     637             : 
     638             :   /// Returns the position within the text for the given pixel offset.
     639           7 :   TextPosition getPositionForOffset(Offset offset) {
     640           7 :     assert(!_needsLayout);
     641          14 :     return _paragraph!.getPositionForOffset(offset);
     642             :   }
     643             : 
     644             :   /// Returns the text range of the word at the given offset. Characters not
     645             :   /// part of a word, such as spaces, symbols, and punctuation, have word breaks
     646             :   /// on both sides. In such cases, this method will return a text range that
     647             :   /// contains the given text position.
     648             :   ///
     649             :   /// Word boundaries are defined more precisely in Unicode Standard Annex #29
     650             :   /// <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
     651           4 :   TextRange getWordBoundary(TextPosition position) {
     652           4 :     assert(!_needsLayout);
     653           8 :     return _paragraph!.getWordBoundary(position);
     654             :   }
     655             : 
     656             :   /// Returns the text range of the line at the given offset.
     657             :   ///
     658             :   /// The newline, if any, is included in the range.
     659           1 :   TextRange getLineBoundary(TextPosition position) {
     660           1 :     assert(!_needsLayout);
     661           2 :     return _paragraph!.getLineBoundary(position);
     662             :   }
     663             : }

Generated by: LCOV version 1.15