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 : }
|