1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:collection';
6import 'dart:math' as math;
7import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Gradient, LineMetrics, PlaceholderAlignment, Shader, TextBox, TextHeightBehavior;
8
9import 'package:flutter/foundation.dart';
10import 'package:flutter/gestures.dart';
11import 'package:flutter/semantics.dart';
12import 'package:flutter/services.dart';
13
14import 'box.dart';
15import 'debug.dart';
16import 'layer.dart';
17import 'layout_helper.dart';
18import 'object.dart';
19import 'selection.dart';
20
21/// The start and end positions for a word.
22typedef _WordBoundaryRecord = ({TextPosition wordStart, TextPosition wordEnd});
23
24const String _kEllipsis = '\u2026';
25
26/// Used by the [RenderParagraph] to map its rendering children to their
27/// corresponding semantics nodes.
28///
29/// The [RichText] uses this to tag the relation between its placeholder spans
30/// and their semantics nodes.
31@immutable
32class PlaceholderSpanIndexSemanticsTag extends SemanticsTag {
33 /// Creates a semantics tag with the input `index`.
34 ///
35 /// Different [PlaceholderSpanIndexSemanticsTag]s with the same `index` are
36 /// consider the same.
37 const PlaceholderSpanIndexSemanticsTag(this.index) : super('PlaceholderSpanIndexSemanticsTag($index)');
38
39 /// The index of this tag.
40 final int index;
41
42 @override
43 bool operator ==(Object other) {
44 return other is PlaceholderSpanIndexSemanticsTag
45 && other.index == index;
46 }
47
48 @override
49 int get hashCode => Object.hash(PlaceholderSpanIndexSemanticsTag, index);
50}
51
52/// Parent data used by [RenderParagraph] and [RenderEditable] to annotate
53/// inline contents (such as [WidgetSpan]s) with.
54class TextParentData extends ParentData with ContainerParentDataMixin<RenderBox> {
55 /// The offset at which to paint the child in the parent's coordinate system.
56 ///
57 /// A `null` value indicates this inline widget is not laid out. For instance,
58 /// when the inline widget has never been laid out, or the inline widget is
59 /// ellipsized away.
60 Offset? get offset => _offset;
61 Offset? _offset;
62
63 /// The [PlaceholderSpan] associated with this render child.
64 ///
65 /// This field is usually set by a [ParentDataWidget], and is typically not
66 /// null when `performLayout` is called.
67 PlaceholderSpan? span;
68
69 @override
70 void detach() {
71 span = null;
72 _offset = null;
73 super.detach();
74 }
75
76 @override
77 String toString() => 'widget: $span, ${offset == null ? "not laid out" : "offset: $offset"}';
78}
79
80/// A mixin that provides useful default behaviors for text [RenderBox]es
81/// ([RenderParagraph] and [RenderEditable] for example) with inline content
82/// children managed by the [ContainerRenderObjectMixin] mixin.
83///
84/// This mixin assumes every child managed by the [ContainerRenderObjectMixin]
85/// mixin corresponds to a [PlaceholderSpan], and they are organized in logical
86/// order of the text (the order each [PlaceholderSpan] is encountered when the
87/// user reads the text).
88///
89/// To use this mixin in a [RenderBox] class:
90///
91/// * Call [layoutInlineChildren] in the `performLayout` and `computeDryLayout`
92/// implementation, and during intrinsic size calculations, to get the size
93/// information of the inline widgets as a `List` of `PlaceholderDimensions`.
94/// Determine the positioning of the inline widgets (which is usually done by
95/// a [TextPainter] using its line break algorithm).
96///
97/// * Call [positionInlineChildren] with the positioning information of the
98/// inline widgets.
99///
100/// * Implement [RenderBox.applyPaintTransform], optionally with
101/// [defaultApplyPaintTransform].
102///
103/// * Call [paintInlineChildren] in [RenderBox.paint] to paint the inline widgets.
104///
105/// * Call [hitTestInlineChildren] in [RenderBox.hitTestChildren] to hit test the
106/// inline widgets.
107///
108/// See also:
109///
110/// * [WidgetSpan.extractFromInlineSpan], a helper function for extracting
111/// [WidgetSpan]s from an [InlineSpan] tree.
112mixin RenderInlineChildrenContainerDefaults on RenderBox, ContainerRenderObjectMixin<RenderBox, TextParentData> {
113 @override
114 void setupParentData(RenderBox child) {
115 if (child.parentData is! TextParentData) {
116 child.parentData = TextParentData();
117 }
118 }
119
120 static PlaceholderDimensions _layoutChild(RenderBox child, double maxWidth, ChildLayouter layoutChild) {
121 final TextParentData parentData = child.parentData! as TextParentData;
122 final PlaceholderSpan? span = parentData.span;
123 assert(span != null);
124 return span == null
125 ? PlaceholderDimensions.empty
126 : PlaceholderDimensions(
127 size: layoutChild(child, BoxConstraints(maxWidth: maxWidth)),
128 alignment: span.alignment,
129 baseline: span.baseline,
130 baselineOffset: switch (span.alignment) {
131 ui.PlaceholderAlignment.aboveBaseline ||
132 ui.PlaceholderAlignment.belowBaseline ||
133 ui.PlaceholderAlignment.bottom ||
134 ui.PlaceholderAlignment.middle ||
135 ui.PlaceholderAlignment.top => null,
136 ui.PlaceholderAlignment.baseline => child.getDistanceToBaseline(span.baseline!),
137 },
138 );
139 }
140
141 /// Computes the layout for every inline child using the given `layoutChild`
142 /// function and the `maxWidth` constraint.
143 ///
144 /// Returns a list of [PlaceholderDimensions], representing the layout results
145 /// for each child managed by the [ContainerRenderObjectMixin] mixin.
146 ///
147 /// Since this method does not impose a maximum height constraint on the
148 /// inline children, some children may become taller than this [RenderBox].
149 ///
150 /// See also:
151 ///
152 /// * [TextPainter.setPlaceholderDimensions], the method that usually takes
153 /// the layout results from this method as the input.
154 @protected
155 List<PlaceholderDimensions> layoutInlineChildren(double maxWidth, ChildLayouter layoutChild) {
156 return <PlaceholderDimensions>[
157 for (RenderBox? child = firstChild; child != null; child = childAfter(child))
158 _layoutChild(child, maxWidth, layoutChild),
159 ];
160 }
161
162 /// Positions each inline child according to the coordinates provided in the
163 /// `boxes` list.
164 ///
165 /// The `boxes` list must be in logical order, which is the order each child
166 /// is encountered when the user reads the text. Usually the length of the
167 /// list equals [childCount], but it can be less than that, when some children
168 /// are ommitted due to ellipsing. It never exceeds [childCount].
169 ///
170 /// See also:
171 ///
172 /// * [TextPainter.inlinePlaceholderBoxes], the method that can be used to
173 /// get the input `boxes`.
174 @protected
175 void positionInlineChildren(List<ui.TextBox> boxes) {
176 RenderBox? child = firstChild;
177 for (final ui.TextBox box in boxes) {
178 if (child == null) {
179 assert(false, 'The length of boxes (${boxes.length}) should be greater than childCount ($childCount)');
180 return;
181 }
182 final TextParentData textParentData = child.parentData! as TextParentData;
183 textParentData._offset = Offset(box.left, box.top);
184 child = childAfter(child);
185 }
186 while (child != null) {
187 final TextParentData textParentData = child.parentData! as TextParentData;
188 textParentData._offset = null;
189 child = childAfter(child);
190 }
191 }
192
193 /// Applies the transform that would be applied when painting the given child
194 /// to the given matrix.
195 ///
196 /// Render children whose [TextParentData.offset] is null zeros out the
197 /// `transform` to indicate they're invisible thus should not be painted.
198 @protected
199 void defaultApplyPaintTransform(RenderBox child, Matrix4 transform) {
200 final TextParentData childParentData = child.parentData! as TextParentData;
201 final Offset? offset = childParentData.offset;
202 if (offset == null) {
203 transform.setZero();
204 } else {
205 transform.translate(offset.dx, offset.dy);
206 }
207 }
208
209 /// Paints each inline child.
210 ///
211 /// Render children whose [TextParentData.offset] is null will be skipped by
212 /// this method.
213 @protected
214 void paintInlineChildren(PaintingContext context, Offset offset) {
215 RenderBox? child = firstChild;
216 while (child != null) {
217 final TextParentData childParentData = child.parentData! as TextParentData;
218 final Offset? childOffset = childParentData.offset;
219 if (childOffset == null) {
220 return;
221 }
222 context.paintChild(child, childOffset + offset);
223 child = childAfter(child);
224 }
225 }
226
227 /// Performs a hit test on each inline child.
228 ///
229 /// Render children whose [TextParentData.offset] is null will be skipped by
230 /// this method.
231 @protected
232 bool hitTestInlineChildren(BoxHitTestResult result, Offset position) {
233 RenderBox? child = firstChild;
234 while (child != null) {
235 final TextParentData childParentData = child.parentData! as TextParentData;
236 final Offset? childOffset = childParentData.offset;
237 if (childOffset == null) {
238 return false;
239 }
240 final bool isHit = result.addWithPaintOffset(
241 offset: childOffset,
242 position: position,
243 hitTest: (BoxHitTestResult result, Offset transformed) => child!.hitTest(result, position: transformed),
244 );
245 if (isHit) {
246 return true;
247 }
248 child = childAfter(child);
249 }
250 return false;
251 }
252}
253
254/// A render object that displays a paragraph of text.
255class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBox, TextParentData>, RenderInlineChildrenContainerDefaults, RelayoutWhenSystemFontsChangeMixin {
256 /// Creates a paragraph render object.
257 ///
258 /// The [maxLines] property may be null (and indeed defaults to null), but if
259 /// it is not null, it must be greater than zero.
260 RenderParagraph(InlineSpan text, {
261 TextAlign textAlign = TextAlign.start,
262 required TextDirection textDirection,
263 bool softWrap = true,
264 TextOverflow overflow = TextOverflow.clip,
265 @Deprecated(
266 'Use textScaler instead. '
267 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
268 'This feature was deprecated after v3.12.0-2.0.pre.',
269 )
270 double textScaleFactor = 1.0,
271 TextScaler textScaler = TextScaler.noScaling,
272 int? maxLines,
273 Locale? locale,
274 StrutStyle? strutStyle,
275 TextWidthBasis textWidthBasis = TextWidthBasis.parent,
276 ui.TextHeightBehavior? textHeightBehavior,
277 List<RenderBox>? children,
278 Color? selectionColor,
279 SelectionRegistrar? registrar,
280 }) : assert(text.debugAssertIsValid()),
281 assert(maxLines == null || maxLines > 0),
282 assert(
283 identical(textScaler, TextScaler.noScaling) || textScaleFactor == 1.0,
284 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
285 ),
286 _softWrap = softWrap,
287 _overflow = overflow,
288 _selectionColor = selectionColor,
289 _textPainter = TextPainter(
290 text: text,
291 textAlign: textAlign,
292 textDirection: textDirection,
293 textScaler: textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler,
294 maxLines: maxLines,
295 ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
296 locale: locale,
297 strutStyle: strutStyle,
298 textWidthBasis: textWidthBasis,
299 textHeightBehavior: textHeightBehavior,
300 ) {
301 addAll(children);
302 this.registrar = registrar;
303 }
304
305 static final String _placeholderCharacter = String.fromCharCode(PlaceholderSpan.placeholderCodeUnit);
306
307 final TextPainter _textPainter;
308
309 List<AttributedString>? _cachedAttributedLabels;
310
311 List<InlineSpanSemanticsInformation>? _cachedCombinedSemanticsInfos;
312
313 /// The text to display.
314 InlineSpan get text => _textPainter.text!;
315 set text(InlineSpan value) {
316 switch (_textPainter.text!.compareTo(value)) {
317 case RenderComparison.identical:
318 return;
319 case RenderComparison.metadata:
320 _textPainter.text = value;
321 _cachedCombinedSemanticsInfos = null;
322 markNeedsSemanticsUpdate();
323 case RenderComparison.paint:
324 _textPainter.text = value;
325 _cachedAttributedLabels = null;
326 _canComputeIntrinsicsCached = null;
327 _cachedCombinedSemanticsInfos = null;
328 markNeedsPaint();
329 markNeedsSemanticsUpdate();
330 case RenderComparison.layout:
331 _textPainter.text = value;
332 _overflowShader = null;
333 _cachedAttributedLabels = null;
334 _cachedCombinedSemanticsInfos = null;
335 _canComputeIntrinsicsCached = null;
336 markNeedsLayout();
337 _removeSelectionRegistrarSubscription();
338 _disposeSelectableFragments();
339 _updateSelectionRegistrarSubscription();
340 }
341 }
342
343 /// The ongoing selections in this paragraph.
344 ///
345 /// The selection does not include selections in [PlaceholderSpan] if there
346 /// are any.
347 @visibleForTesting
348 List<TextSelection> get selections {
349 if (_lastSelectableFragments == null) {
350 return const <TextSelection>[];
351 }
352 final List<TextSelection> results = <TextSelection>[];
353 for (final _SelectableFragment fragment in _lastSelectableFragments!) {
354 if (fragment._textSelectionStart != null &&
355 fragment._textSelectionEnd != null) {
356 results.add(
357 TextSelection(
358 baseOffset: fragment._textSelectionStart!.offset,
359 extentOffset: fragment._textSelectionEnd!.offset
360 )
361 );
362 }
363 }
364 return results;
365 }
366
367 // Should be null if selection is not enabled, i.e. _registrar = null. The
368 // paragraph splits on [PlaceholderSpan.placeholderCodeUnit], and stores each
369 // fragment in this list.
370 List<_SelectableFragment>? _lastSelectableFragments;
371
372 /// The [SelectionRegistrar] this paragraph will be, or is, registered to.
373 SelectionRegistrar? get registrar => _registrar;
374 SelectionRegistrar? _registrar;
375 set registrar(SelectionRegistrar? value) {
376 if (value == _registrar) {
377 return;
378 }
379 _removeSelectionRegistrarSubscription();
380 _disposeSelectableFragments();
381 _registrar = value;
382 _updateSelectionRegistrarSubscription();
383 }
384
385 void _updateSelectionRegistrarSubscription() {
386 if (_registrar == null) {
387 return;
388 }
389 _lastSelectableFragments ??= _getSelectableFragments();
390 _lastSelectableFragments!.forEach(_registrar!.add);
391 if (_lastSelectableFragments!.isNotEmpty) {
392 markNeedsCompositingBitsUpdate();
393 }
394 }
395
396 void _removeSelectionRegistrarSubscription() {
397 if (_registrar == null || _lastSelectableFragments == null) {
398 return;
399 }
400 _lastSelectableFragments!.forEach(_registrar!.remove);
401 }
402
403 List<_SelectableFragment> _getSelectableFragments() {
404 final String plainText = text.toPlainText(includeSemanticsLabels: false);
405 final List<_SelectableFragment> result = <_SelectableFragment>[];
406 int start = 0;
407 while (start < plainText.length) {
408 int end = plainText.indexOf(_placeholderCharacter, start);
409 if (start != end) {
410 if (end == -1) {
411 end = plainText.length;
412 }
413 result.add(
414 _SelectableFragment(
415 paragraph: this,
416 range: TextRange(start: start, end: end),
417 fullText: plainText,
418 ),
419 );
420 start = end;
421 }
422 start += 1;
423 }
424 return result;
425 }
426
427 void _disposeSelectableFragments() {
428 if (_lastSelectableFragments == null) {
429 return;
430 }
431 for (final _SelectableFragment fragment in _lastSelectableFragments!) {
432 fragment.dispose();
433 }
434 _lastSelectableFragments = null;
435 }
436
437 @override
438 bool get alwaysNeedsCompositing => _lastSelectableFragments?.isNotEmpty ?? false;
439
440 @override
441 void markNeedsLayout() {
442 _lastSelectableFragments?.forEach((_SelectableFragment element) => element.didChangeParagraphLayout());
443 super.markNeedsLayout();
444 }
445
446 @override
447 void dispose() {
448 _removeSelectionRegistrarSubscription();
449 _disposeSelectableFragments();
450 _textPainter.dispose();
451 super.dispose();
452 }
453
454 /// How the text should be aligned horizontally.
455 TextAlign get textAlign => _textPainter.textAlign;
456 set textAlign(TextAlign value) {
457 if (_textPainter.textAlign == value) {
458 return;
459 }
460 _textPainter.textAlign = value;
461 markNeedsPaint();
462 }
463
464 /// The directionality of the text.
465 ///
466 /// This decides how the [TextAlign.start], [TextAlign.end], and
467 /// [TextAlign.justify] values of [textAlign] are interpreted.
468 ///
469 /// This is also used to disambiguate how to render bidirectional text. For
470 /// example, if the [text] is an English phrase followed by a Hebrew phrase,
471 /// in a [TextDirection.ltr] context the English phrase will be on the left
472 /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
473 /// context, the English phrase will be on the right and the Hebrew phrase on
474 /// its left.
475 TextDirection get textDirection => _textPainter.textDirection!;
476 set textDirection(TextDirection value) {
477 if (_textPainter.textDirection == value) {
478 return;
479 }
480 _textPainter.textDirection = value;
481 markNeedsLayout();
482 }
483
484 /// Whether the text should break at soft line breaks.
485 ///
486 /// If false, the glyphs in the text will be positioned as if there was
487 /// unlimited horizontal space.
488 ///
489 /// If [softWrap] is false, [overflow] and [textAlign] may have unexpected
490 /// effects.
491 bool get softWrap => _softWrap;
492 bool _softWrap;
493 set softWrap(bool value) {
494 if (_softWrap == value) {
495 return;
496 }
497 _softWrap = value;
498 markNeedsLayout();
499 }
500
501 /// How visual overflow should be handled.
502 TextOverflow get overflow => _overflow;
503 TextOverflow _overflow;
504 set overflow(TextOverflow value) {
505 if (_overflow == value) {
506 return;
507 }
508 _overflow = value;
509 _textPainter.ellipsis = value == TextOverflow.ellipsis ? _kEllipsis : null;
510 markNeedsLayout();
511 }
512
513 /// Deprecated. Will be removed in a future version of Flutter. Use
514 /// [textScaler] instead.
515 ///
516 /// The number of font pixels for each logical pixel.
517 ///
518 /// For example, if the text scale factor is 1.5, text will be 50% larger than
519 /// the specified font size.
520 @Deprecated(
521 'Use textScaler instead. '
522 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
523 'This feature was deprecated after v3.12.0-2.0.pre.',
524 )
525 double get textScaleFactor => _textPainter.textScaleFactor;
526 @Deprecated(
527 'Use textScaler instead. '
528 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
529 'This feature was deprecated after v3.12.0-2.0.pre.',
530 )
531 set textScaleFactor(double value) {
532 textScaler = TextScaler.linear(value);
533 }
534
535 /// {@macro flutter.painting.textPainter.textScaler}
536 TextScaler get textScaler => _textPainter.textScaler;
537 set textScaler(TextScaler value) {
538 if (_textPainter.textScaler == value) {
539 return;
540 }
541 _textPainter.textScaler = value;
542 _overflowShader = null;
543 markNeedsLayout();
544 }
545
546 /// An optional maximum number of lines for the text to span, wrapping if
547 /// necessary. If the text exceeds the given number of lines, it will be
548 /// truncated according to [overflow] and [softWrap].
549 int? get maxLines => _textPainter.maxLines;
550 /// The value may be null. If it is not null, then it must be greater than
551 /// zero.
552 set maxLines(int? value) {
553 assert(value == null || value > 0);
554 if (_textPainter.maxLines == value) {
555 return;
556 }
557 _textPainter.maxLines = value;
558 _overflowShader = null;
559 markNeedsLayout();
560 }
561
562 /// Used by this paragraph's internal [TextPainter] to select a
563 /// locale-specific font.
564 ///
565 /// In some cases, the same Unicode character may be rendered differently
566 /// depending on the locale. For example, the '骨' character is rendered
567 /// differently in the Chinese and Japanese locales. In these cases, the
568 /// [locale] may be used to select a locale-specific font.
569 Locale? get locale => _textPainter.locale;
570 /// The value may be null.
571 set locale(Locale? value) {
572 if (_textPainter.locale == value) {
573 return;
574 }
575 _textPainter.locale = value;
576 _overflowShader = null;
577 markNeedsLayout();
578 }
579
580 /// {@macro flutter.painting.textPainter.strutStyle}
581 StrutStyle? get strutStyle => _textPainter.strutStyle;
582 /// The value may be null.
583 set strutStyle(StrutStyle? value) {
584 if (_textPainter.strutStyle == value) {
585 return;
586 }
587 _textPainter.strutStyle = value;
588 _overflowShader = null;
589 markNeedsLayout();
590 }
591
592 /// {@macro flutter.painting.textPainter.textWidthBasis}
593 TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis;
594 set textWidthBasis(TextWidthBasis value) {
595 if (_textPainter.textWidthBasis == value) {
596 return;
597 }
598 _textPainter.textWidthBasis = value;
599 _overflowShader = null;
600 markNeedsLayout();
601 }
602
603 /// {@macro dart.ui.textHeightBehavior}
604 ui.TextHeightBehavior? get textHeightBehavior => _textPainter.textHeightBehavior;
605 set textHeightBehavior(ui.TextHeightBehavior? value) {
606 if (_textPainter.textHeightBehavior == value) {
607 return;
608 }
609 _textPainter.textHeightBehavior = value;
610 _overflowShader = null;
611 markNeedsLayout();
612 }
613
614 /// The color to use when painting the selection.
615 ///
616 /// Ignored if the text is not selectable (e.g. if [registrar] is null).
617 Color? get selectionColor => _selectionColor;
618 Color? _selectionColor;
619 set selectionColor(Color? value) {
620 if (_selectionColor == value) {
621 return;
622 }
623 _selectionColor = value;
624 if (_lastSelectableFragments?.any((_SelectableFragment fragment) => fragment.value.hasSelection) ?? false) {
625 markNeedsPaint();
626 }
627 }
628
629 Offset _getOffsetForPosition(TextPosition position) {
630 return getOffsetForCaret(position, Rect.zero) + Offset(0, getFullHeightForCaret(position) ?? 0.0);
631 }
632
633 List<ui.LineMetrics> _computeLineMetrics() {
634 return _textPainter.computeLineMetrics();
635 }
636
637 @override
638 double computeMinIntrinsicWidth(double height) {
639 if (!_canComputeIntrinsics()) {
640 return 0.0;
641 }
642 _textPainter.setPlaceholderDimensions(layoutInlineChildren(
643 double.infinity,
644 (RenderBox child, BoxConstraints constraints) => Size(child.getMinIntrinsicWidth(double.infinity), 0.0),
645 ));
646 _layoutText(); // layout with infinite width.
647 return _textPainter.minIntrinsicWidth;
648 }
649
650 @override
651 double computeMaxIntrinsicWidth(double height) {
652 if (!_canComputeIntrinsics()) {
653 return 0.0;
654 }
655 _textPainter.setPlaceholderDimensions(layoutInlineChildren(
656 double.infinity,
657 // Height and baseline is irrelevant as all text will be laid
658 // out in a single line. Therefore, using 0.0 as a dummy for the height.
659 (RenderBox child, BoxConstraints constraints) => Size(child.getMaxIntrinsicWidth(double.infinity), 0.0),
660 ));
661 _layoutText(); // layout with infinite width.
662 return _textPainter.maxIntrinsicWidth;
663 }
664
665 double _computeIntrinsicHeight(double width) {
666 if (!_canComputeIntrinsics()) {
667 return 0.0;
668 }
669 _textPainter.setPlaceholderDimensions(layoutInlineChildren(width, ChildLayoutHelper.dryLayoutChild));
670 _layoutText(minWidth: width, maxWidth: width);
671 return _textPainter.height;
672 }
673
674 @override
675 double computeMinIntrinsicHeight(double width) {
676 return _computeIntrinsicHeight(width);
677 }
678
679 @override
680 double computeMaxIntrinsicHeight(double width) {
681 return _computeIntrinsicHeight(width);
682 }
683
684 @override
685 double computeDistanceToActualBaseline(TextBaseline baseline) {
686 assert(!debugNeedsLayout);
687 assert(constraints.debugAssertIsValid());
688 _layoutTextWithConstraints(constraints);
689 // TODO(garyq): Since our metric for ideographic baseline is currently
690 // inaccurate and the non-alphabetic baselines are based off of the
691 // alphabetic baseline, we use the alphabetic for now to produce correct
692 // layouts. We should eventually change this back to pass the `baseline`
693 // property when the ideographic baseline is properly implemented
694 // (https://github.com/flutter/flutter/issues/22625).
695 return _textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic);
696 }
697
698 /// Whether all inline widget children of this [RenderBox] support dry layout
699 /// calculation.
700 bool _canComputeDryLayoutForInlineWidgets() {
701 // Dry layout cannot be calculated without a full layout for
702 // alignments that require the baseline (baseline, aboveBaseline,
703 // belowBaseline).
704 return text.visitChildren((InlineSpan span) {
705 return (span is! PlaceholderSpan) || switch (span.alignment) {
706 ui.PlaceholderAlignment.baseline ||
707 ui.PlaceholderAlignment.aboveBaseline ||
708 ui.PlaceholderAlignment.belowBaseline => false,
709 ui.PlaceholderAlignment.top ||
710 ui.PlaceholderAlignment.middle ||
711 ui.PlaceholderAlignment.bottom => true,
712 };
713 });
714 }
715
716 bool? _canComputeIntrinsicsCached;
717 // Intrinsics cannot be calculated without a full layout for
718 // alignments that require the baseline (baseline, aboveBaseline,
719 // belowBaseline).
720 bool _canComputeIntrinsics() {
721 final bool returnValue = _canComputeIntrinsicsCached ??= _canComputeDryLayoutForInlineWidgets();
722 assert(
723 returnValue || RenderObject.debugCheckingIntrinsics,
724 'Intrinsics are not available for PlaceholderAlignment.baseline, '
725 'PlaceholderAlignment.aboveBaseline, or PlaceholderAlignment.belowBaseline.',
726 );
727 return returnValue;
728 }
729
730 @override
731 bool hitTestSelf(Offset position) => true;
732
733 @override
734 @protected
735 bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
736 final GlyphInfo? glyph = _textPainter.getClosestGlyphForOffset(position);
737 // The hit-test can't fall through the horizontal gaps between visually
738 // adjacent characters on the same line, even with a large letter-spacing or
739 // text justification, as graphemeClusterLayoutBounds.width is the advance
740 // width to the next character, so there's no gap between their
741 // graphemeClusterLayoutBounds rects.
742 final InlineSpan? spanHit = glyph != null && glyph.graphemeClusterLayoutBounds.contains(position)
743 ? _textPainter.text!.getSpanForPosition(TextPosition(offset: glyph.graphemeClusterCodeUnitRange.start))
744 : null;
745 switch (spanHit) {
746 case final HitTestTarget span:
747 result.add(HitTestEntry(span));
748 return true;
749 case _:
750 return hitTestInlineChildren(result, position);
751 }
752 }
753
754 bool _needsClipping = false;
755 ui.Shader? _overflowShader;
756
757 /// Whether this paragraph currently has a [dart:ui.Shader] for its overflow
758 /// effect.
759 ///
760 /// Used to test this object. Not for use in production.
761 @visibleForTesting
762 bool get debugHasOverflowShader => _overflowShader != null;
763
764 void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
765 final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
766 _textPainter.layout(
767 minWidth: minWidth,
768 maxWidth: widthMatters ? maxWidth : double.infinity,
769 );
770 }
771
772 @override
773 void systemFontsDidChange() {
774 super.systemFontsDidChange();
775 _textPainter.markNeedsLayout();
776 }
777
778 // Placeholder dimensions representing the sizes of child inline widgets.
779 //
780 // These need to be cached because the text painter's placeholder dimensions
781 // will be overwritten during intrinsic width/height calculations and must be
782 // restored to the original values before final layout and painting.
783 List<PlaceholderDimensions>? _placeholderDimensions;
784
785 void _layoutTextWithConstraints(BoxConstraints constraints) {
786 _textPainter.setPlaceholderDimensions(_placeholderDimensions);
787 _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
788 }
789
790 @override
791 @protected
792 Size computeDryLayout(covariant BoxConstraints constraints) {
793 if (!_canComputeIntrinsics()) {
794 assert(debugCannotComputeDryLayout(
795 reason: 'Dry layout not available for alignments that require baseline.',
796 ));
797 return Size.zero;
798 }
799 _textPainter.setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild));
800 _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
801 return constraints.constrain(_textPainter.size);
802 }
803
804 @override
805 void performLayout() {
806 final BoxConstraints constraints = this.constraints;
807 _placeholderDimensions = layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.layoutChild);
808 _layoutTextWithConstraints(constraints);
809 positionInlineChildren(_textPainter.inlinePlaceholderBoxes!);
810
811 // We grab _textPainter.size and _textPainter.didExceedMaxLines here because
812 // assigning to `size` will trigger us to validate our intrinsic sizes,
813 // which will change _textPainter's layout because the intrinsic size
814 // calculations are destructive. Other _textPainter state will also be
815 // affected. See also RenderEditable which has a similar issue.
816 final Size textSize = _textPainter.size;
817 final bool textDidExceedMaxLines = _textPainter.didExceedMaxLines;
818 size = constraints.constrain(textSize);
819
820 final bool didOverflowHeight = size.height < textSize.height || textDidExceedMaxLines;
821 final bool didOverflowWidth = size.width < textSize.width;
822 // TODO(abarth): We're only measuring the sizes of the line boxes here. If
823 // the glyphs draw outside the line boxes, we might think that there isn't
824 // visual overflow when there actually is visual overflow. This can become
825 // a problem if we start having horizontal overflow and introduce a clip
826 // that affects the actual (but undetected) vertical overflow.
827 final bool hasVisualOverflow = didOverflowWidth || didOverflowHeight;
828 if (hasVisualOverflow) {
829 switch (_overflow) {
830 case TextOverflow.visible:
831 _needsClipping = false;
832 _overflowShader = null;
833 case TextOverflow.clip:
834 case TextOverflow.ellipsis:
835 _needsClipping = true;
836 _overflowShader = null;
837 case TextOverflow.fade:
838 _needsClipping = true;
839 final TextPainter fadeSizePainter = TextPainter(
840 text: TextSpan(style: _textPainter.text!.style, text: '\u2026'),
841 textDirection: textDirection,
842 textScaler: textScaler,
843 locale: locale,
844 )..layout();
845 if (didOverflowWidth) {
846 double fadeEnd, fadeStart;
847 switch (textDirection) {
848 case TextDirection.rtl:
849 fadeEnd = 0.0;
850 fadeStart = fadeSizePainter.width;
851 case TextDirection.ltr:
852 fadeEnd = size.width;
853 fadeStart = fadeEnd - fadeSizePainter.width;
854 }
855 _overflowShader = ui.Gradient.linear(
856 Offset(fadeStart, 0.0),
857 Offset(fadeEnd, 0.0),
858 <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
859 );
860 } else {
861 final double fadeEnd = size.height;
862 final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0;
863 _overflowShader = ui.Gradient.linear(
864 Offset(0.0, fadeStart),
865 Offset(0.0, fadeEnd),
866 <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
867 );
868 }
869 fadeSizePainter.dispose();
870 }
871 } else {
872 _needsClipping = false;
873 _overflowShader = null;
874 }
875 }
876
877 @override
878 void applyPaintTransform(RenderBox child, Matrix4 transform) {
879 defaultApplyPaintTransform(child, transform);
880 }
881
882 @override
883 void paint(PaintingContext context, Offset offset) {
884 // Ideally we could compute the min/max intrinsic width/height with a
885 // non-destructive operation. However, currently, computing these values
886 // will destroy state inside the painter. If that happens, we need to get
887 // back the correct state by calling _layout again.
888 //
889 // TODO(abarth): Make computing the min/max intrinsic width/height a
890 // non-destructive operation.
891 //
892 // If you remove this call, make sure that changing the textAlign still
893 // works properly.
894 _layoutTextWithConstraints(constraints);
895
896 assert(() {
897 if (debugRepaintTextRainbowEnabled) {
898 final Paint paint = Paint()
899 ..color = debugCurrentRepaintColor.toColor();
900 context.canvas.drawRect(offset & size, paint);
901 }
902 return true;
903 }());
904
905 if (_needsClipping) {
906 final Rect bounds = offset & size;
907 if (_overflowShader != null) {
908 // This layer limits what the shader below blends with to be just the
909 // text (as opposed to the text and its background).
910 context.canvas.saveLayer(bounds, Paint());
911 } else {
912 context.canvas.save();
913 }
914 context.canvas.clipRect(bounds);
915 }
916
917 if (_lastSelectableFragments != null) {
918 for (final _SelectableFragment fragment in _lastSelectableFragments!) {
919 fragment.paint(context, offset);
920 }
921 }
922
923 _textPainter.paint(context.canvas, offset);
924
925 paintInlineChildren(context, offset);
926
927 if (_needsClipping) {
928 if (_overflowShader != null) {
929 context.canvas.translate(offset.dx, offset.dy);
930 final Paint paint = Paint()
931 ..blendMode = BlendMode.modulate
932 ..shader = _overflowShader;
933 context.canvas.drawRect(Offset.zero & size, paint);
934 }
935 context.canvas.restore();
936 }
937 }
938
939 /// Returns the offset at which to paint the caret.
940 ///
941 /// Valid only after [layout].
942 Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
943 assert(!debugNeedsLayout);
944 _layoutTextWithConstraints(constraints);
945 return _textPainter.getOffsetForCaret(position, caretPrototype);
946 }
947
948 /// {@macro flutter.painting.textPainter.getFullHeightForCaret}
949 ///
950 /// Valid only after [layout].
951 double? getFullHeightForCaret(TextPosition position) {
952 assert(!debugNeedsLayout);
953 _layoutTextWithConstraints(constraints);
954 return _textPainter.getFullHeightForCaret(position, Rect.zero);
955 }
956
957 /// Returns a list of rects that bound the given selection.
958 ///
959 /// The [boxHeightStyle] and [boxWidthStyle] arguments may be used to select
960 /// the shape of the [TextBox]es. These properties default to
961 /// [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively.
962 ///
963 /// A given selection might have more than one rect if the [RenderParagraph]
964 /// contains multiple [InlineSpan]s or bidirectional text, because logically
965 /// contiguous text might not be visually contiguous.
966 ///
967 /// Valid only after [layout].
968 ///
969 /// See also:
970 ///
971 /// * [TextPainter.getBoxesForSelection], the method in TextPainter to get
972 /// the equivalent boxes.
973 List<ui.TextBox> getBoxesForSelection(
974 TextSelection selection, {
975 ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
976 ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
977 }) {
978 assert(!debugNeedsLayout);
979 _layoutTextWithConstraints(constraints);
980 return _textPainter.getBoxesForSelection(
981 selection,
982 boxHeightStyle: boxHeightStyle,
983 boxWidthStyle: boxWidthStyle,
984 );
985 }
986
987 /// Returns the position within the text for the given pixel offset.
988 ///
989 /// Valid only after [layout].
990 TextPosition getPositionForOffset(Offset offset) {
991 assert(!debugNeedsLayout);
992 _layoutTextWithConstraints(constraints);
993 return _textPainter.getPositionForOffset(offset);
994 }
995
996 /// Returns the text range of the word at the given offset. Characters not
997 /// part of a word, such as spaces, symbols, and punctuation, have word breaks
998 /// on both sides. In such cases, this method will return a text range that
999 /// contains the given text position.
1000 ///
1001 /// Word boundaries are defined more precisely in Unicode Standard Annex #29
1002 /// <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
1003 ///
1004 /// Valid only after [layout].
1005 TextRange getWordBoundary(TextPosition position) {
1006 assert(!debugNeedsLayout);
1007 _layoutTextWithConstraints(constraints);
1008 return _textPainter.getWordBoundary(position);
1009 }
1010
1011 TextRange _getLineAtOffset(TextPosition position) => _textPainter.getLineBoundary(position);
1012
1013 TextPosition _getTextPositionAbove(TextPosition position) {
1014 // -0.5 of preferredLineHeight points to the middle of the line above.
1015 final double preferredLineHeight = _textPainter.preferredLineHeight;
1016 final double verticalOffset = -0.5 * preferredLineHeight;
1017 return _getTextPositionVertical(position, verticalOffset);
1018 }
1019
1020 TextPosition _getTextPositionBelow(TextPosition position) {
1021 // 1.5 of preferredLineHeight points to the middle of the line below.
1022 final double preferredLineHeight = _textPainter.preferredLineHeight;
1023 final double verticalOffset = 1.5 * preferredLineHeight;
1024 return _getTextPositionVertical(position, verticalOffset);
1025 }
1026
1027 TextPosition _getTextPositionVertical(TextPosition position, double verticalOffset) {
1028 final Offset caretOffset = _textPainter.getOffsetForCaret(position, Rect.zero);
1029 final Offset caretOffsetTranslated = caretOffset.translate(0.0, verticalOffset);
1030 return _textPainter.getPositionForOffset(caretOffsetTranslated);
1031 }
1032
1033 /// Returns the size of the text as laid out.
1034 ///
1035 /// This can differ from [size] if the text overflowed or if the [constraints]
1036 /// provided by the parent [RenderObject] forced the layout to be bigger than
1037 /// necessary for the given [text].
1038 ///
1039 /// This returns the [TextPainter.size] of the underlying [TextPainter].
1040 ///
1041 /// Valid only after [layout].
1042 Size get textSize {
1043 assert(!debugNeedsLayout);
1044 return _textPainter.size;
1045 }
1046
1047 /// Whether the text was truncated or ellipsized as laid out.
1048 ///
1049 /// This returns the [TextPainter.didExceedMaxLines] of the underlying [TextPainter].
1050 ///
1051 /// Valid only after [layout].
1052 bool get didExceedMaxLines {
1053 assert(!debugNeedsLayout);
1054 return _textPainter.didExceedMaxLines;
1055 }
1056
1057 /// Collected during [describeSemanticsConfiguration], used by
1058 /// [assembleSemanticsNode] and [_combineSemanticsInfo].
1059 List<InlineSpanSemanticsInformation>? _semanticsInfo;
1060
1061 @override
1062 void describeSemanticsConfiguration(SemanticsConfiguration config) {
1063 super.describeSemanticsConfiguration(config);
1064 _semanticsInfo = text.getSemanticsInformation();
1065 bool needsAssembleSemanticsNode = false;
1066 bool needsChildConfigrationsDelegate = false;
1067 for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
1068 if (info.recognizer != null) {
1069 needsAssembleSemanticsNode = true;
1070 break;
1071 }
1072 needsChildConfigrationsDelegate = needsChildConfigrationsDelegate || info.isPlaceholder;
1073 }
1074
1075 if (needsAssembleSemanticsNode) {
1076 config.explicitChildNodes = true;
1077 config.isSemanticBoundary = true;
1078 } else if (needsChildConfigrationsDelegate) {
1079 config.childConfigurationsDelegate = _childSemanticsConfigurationsDelegate;
1080 } else {
1081 if (_cachedAttributedLabels == null) {
1082 final StringBuffer buffer = StringBuffer();
1083 int offset = 0;
1084 final List<StringAttribute> attributes = <StringAttribute>[];
1085 for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
1086 final String label = info.semanticsLabel ?? info.text;
1087 for (final StringAttribute infoAttribute in info.stringAttributes) {
1088 final TextRange originalRange = infoAttribute.range;
1089 attributes.add(
1090 infoAttribute.copy(
1091 range: TextRange(
1092 start: offset + originalRange.start,
1093 end: offset + originalRange.end,
1094 ),
1095 ),
1096 );
1097 }
1098 buffer.write(label);
1099 offset += label.length;
1100 }
1101 _cachedAttributedLabels = <AttributedString>[AttributedString(buffer.toString(), attributes: attributes)];
1102 }
1103 config.attributedLabel = _cachedAttributedLabels![0];
1104 config.textDirection = textDirection;
1105 }
1106 }
1107
1108 ChildSemanticsConfigurationsResult _childSemanticsConfigurationsDelegate(List<SemanticsConfiguration> childConfigs) {
1109 final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
1110 int placeholderIndex = 0;
1111 int childConfigsIndex = 0;
1112 int attributedLabelCacheIndex = 0;
1113 InlineSpanSemanticsInformation? seenTextInfo;
1114 _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
1115 for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) {
1116 if (info.isPlaceholder) {
1117 if (seenTextInfo != null) {
1118 builder.markAsMergeUp(_createSemanticsConfigForTextInfo(seenTextInfo, attributedLabelCacheIndex));
1119 attributedLabelCacheIndex += 1;
1120 }
1121 // Mark every childConfig belongs to this placeholder to merge up group.
1122 while (childConfigsIndex < childConfigs.length &&
1123 childConfigs[childConfigsIndex].tagsChildrenWith(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
1124 builder.markAsMergeUp(childConfigs[childConfigsIndex]);
1125 childConfigsIndex += 1;
1126 }
1127 placeholderIndex += 1;
1128 } else {
1129 seenTextInfo = info;
1130 }
1131 }
1132
1133 // Handle plain text info at the end.
1134 if (seenTextInfo != null) {
1135 builder.markAsMergeUp(_createSemanticsConfigForTextInfo(seenTextInfo, attributedLabelCacheIndex));
1136 }
1137 return builder.build();
1138 }
1139
1140 SemanticsConfiguration _createSemanticsConfigForTextInfo(InlineSpanSemanticsInformation textInfo, int cacheIndex) {
1141 assert(!textInfo.requiresOwnNode);
1142 final List<AttributedString> cachedStrings = _cachedAttributedLabels ??= <AttributedString>[];
1143 assert(cacheIndex <= cachedStrings.length);
1144 final bool hasCache = cacheIndex < cachedStrings.length;
1145
1146 late AttributedString attributedLabel;
1147 if (hasCache) {
1148 attributedLabel = cachedStrings[cacheIndex];
1149 } else {
1150 assert(cachedStrings.length == cacheIndex);
1151 attributedLabel = AttributedString(
1152 textInfo.semanticsLabel ?? textInfo.text,
1153 attributes: textInfo.stringAttributes,
1154 );
1155 cachedStrings.add(attributedLabel);
1156 }
1157 return SemanticsConfiguration()
1158 ..textDirection = textDirection
1159 ..attributedLabel = attributedLabel;
1160 }
1161
1162 // Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
1163 // can be re-used when [assembleSemanticsNode] is called again. This ensures
1164 // stable ids for the [SemanticsNode]s of [TextSpan]s across
1165 // [assembleSemanticsNode] invocations.
1166 LinkedHashMap<Key, SemanticsNode>? _cachedChildNodes;
1167
1168 @override
1169 void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
1170 assert(_semanticsInfo != null && _semanticsInfo!.isNotEmpty);
1171 final List<SemanticsNode> newChildren = <SemanticsNode>[];
1172 TextDirection currentDirection = textDirection;
1173 Rect currentRect;
1174 double ordinal = 0.0;
1175 int start = 0;
1176 int placeholderIndex = 0;
1177 int childIndex = 0;
1178 RenderBox? child = firstChild;
1179 final LinkedHashMap<Key, SemanticsNode> newChildCache = LinkedHashMap<Key, SemanticsNode>();
1180 _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
1181 for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) {
1182 final TextSelection selection = TextSelection(
1183 baseOffset: start,
1184 extentOffset: start + info.text.length,
1185 );
1186 start += info.text.length;
1187
1188 if (info.isPlaceholder) {
1189 // A placeholder span may have 0 to multiple semantics nodes, we need
1190 // to annotate all of the semantics nodes belong to this span.
1191 while (children.length > childIndex &&
1192 children.elementAt(childIndex).isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
1193 final SemanticsNode childNode = children.elementAt(childIndex);
1194 final TextParentData parentData = child!.parentData! as TextParentData;
1195 // parentData.scale may be null if the render object is truncated.
1196 if (parentData.offset != null) {
1197 newChildren.add(childNode);
1198 }
1199 childIndex += 1;
1200 }
1201 child = childAfter(child!);
1202 placeholderIndex += 1;
1203 } else {
1204 final TextDirection initialDirection = currentDirection;
1205 final List<ui.TextBox> rects = getBoxesForSelection(selection);
1206 if (rects.isEmpty) {
1207 continue;
1208 }
1209 Rect rect = rects.first.toRect();
1210 currentDirection = rects.first.direction;
1211 for (final ui.TextBox textBox in rects.skip(1)) {
1212 rect = rect.expandToInclude(textBox.toRect());
1213 currentDirection = textBox.direction;
1214 }
1215 // Any of the text boxes may have had infinite dimensions.
1216 // We shouldn't pass infinite dimensions up to the bridges.
1217 rect = Rect.fromLTWH(
1218 math.max(0.0, rect.left),
1219 math.max(0.0, rect.top),
1220 math.min(rect.width, constraints.maxWidth),
1221 math.min(rect.height, constraints.maxHeight),
1222 );
1223 // round the current rectangle to make this API testable and add some
1224 // padding so that the accessibility rects do not overlap with the text.
1225 currentRect = Rect.fromLTRB(
1226 rect.left.floorToDouble() - 4.0,
1227 rect.top.floorToDouble() - 4.0,
1228 rect.right.ceilToDouble() + 4.0,
1229 rect.bottom.ceilToDouble() + 4.0,
1230 );
1231 final SemanticsConfiguration configuration = SemanticsConfiguration()
1232 ..sortKey = OrdinalSortKey(ordinal++)
1233 ..textDirection = initialDirection
1234 ..attributedLabel = AttributedString(info.semanticsLabel ?? info.text, attributes: info.stringAttributes);
1235 final GestureRecognizer? recognizer = info.recognizer;
1236 if (recognizer != null) {
1237 if (recognizer is TapGestureRecognizer) {
1238 if (recognizer.onTap != null) {
1239 configuration.onTap = recognizer.onTap;
1240 configuration.isLink = true;
1241 }
1242 } else if (recognizer is DoubleTapGestureRecognizer) {
1243 if (recognizer.onDoubleTap != null) {
1244 configuration.onTap = recognizer.onDoubleTap;
1245 configuration.isLink = true;
1246 }
1247 } else if (recognizer is LongPressGestureRecognizer) {
1248 if (recognizer.onLongPress != null) {
1249 configuration.onLongPress = recognizer.onLongPress;
1250 }
1251 } else {
1252 assert(false, '${recognizer.runtimeType} is not supported.');
1253 }
1254 }
1255 if (node.parentPaintClipRect != null) {
1256 final Rect paintRect = node.parentPaintClipRect!.intersect(currentRect);
1257 configuration.isHidden = paintRect.isEmpty && !currentRect.isEmpty;
1258 }
1259 final SemanticsNode newChild;
1260 if (_cachedChildNodes?.isNotEmpty ?? false) {
1261 newChild = _cachedChildNodes!.remove(_cachedChildNodes!.keys.first)!;
1262 } else {
1263 final UniqueKey key = UniqueKey();
1264 newChild = SemanticsNode(
1265 key: key,
1266 showOnScreen: _createShowOnScreenFor(key),
1267 );
1268 }
1269 newChild
1270 ..updateWith(config: configuration)
1271 ..rect = currentRect;
1272 newChildCache[newChild.key!] = newChild;
1273 newChildren.add(newChild);
1274 }
1275 }
1276 // Makes sure we annotated all of the semantics children.
1277 assert(childIndex == children.length);
1278 assert(child == null);
1279
1280 _cachedChildNodes = newChildCache;
1281 node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
1282 }
1283
1284 VoidCallback? _createShowOnScreenFor(Key key) {
1285 return () {
1286 final SemanticsNode node = _cachedChildNodes![key]!;
1287 showOnScreen(descendant: this, rect: node.rect);
1288 };
1289 }
1290
1291 @override
1292 void clearSemantics() {
1293 super.clearSemantics();
1294 _cachedChildNodes = null;
1295 }
1296
1297 @override
1298 List<DiagnosticsNode> debugDescribeChildren() {
1299 return <DiagnosticsNode>[
1300 text.toDiagnosticsNode(
1301 name: 'text',
1302 style: DiagnosticsTreeStyle.transition,
1303 ),
1304 ];
1305 }
1306
1307 @override
1308 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1309 super.debugFillProperties(properties);
1310 properties.add(EnumProperty<TextAlign>('textAlign', textAlign));
1311 properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
1312 properties.add(
1313 FlagProperty(
1314 'softWrap',
1315 value: softWrap,
1316 ifTrue: 'wrapping at box width',
1317 ifFalse: 'no wrapping except at line break characters',
1318 showName: true,
1319 ),
1320 );
1321 properties.add(EnumProperty<TextOverflow>('overflow', overflow));
1322 properties.add(
1323 DiagnosticsProperty<TextScaler>('textScaler', textScaler, defaultValue: TextScaler.noScaling),
1324 );
1325 properties.add(
1326 DiagnosticsProperty<Locale>(
1327 'locale',
1328 locale,
1329 defaultValue: null,
1330 ),
1331 );
1332 properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
1333 }
1334}
1335
1336/// A continuous, selectable piece of paragraph.
1337///
1338/// Since the selections in [PlaceholderSpan] are handled independently in its
1339/// subtree, a selection in [RenderParagraph] can't continue across a
1340/// [PlaceholderSpan]. The [RenderParagraph] splits itself on [PlaceholderSpan]
1341/// to create multiple `_SelectableFragment`s so that they can be selected
1342/// separately.
1343class _SelectableFragment with Selectable, Diagnosticable, ChangeNotifier implements TextLayoutMetrics {
1344 _SelectableFragment({
1345 required this.paragraph,
1346 required this.fullText,
1347 required this.range,
1348 }) : assert(range.isValid && !range.isCollapsed && range.isNormalized) {
1349 if (kFlutterMemoryAllocationsEnabled) {
1350 ChangeNotifier.maybeDispatchObjectCreation(this);
1351 }
1352 _selectionGeometry = _getSelectionGeometry();
1353 }
1354
1355 final TextRange range;
1356 final RenderParagraph paragraph;
1357 final String fullText;
1358
1359 TextPosition? _textSelectionStart;
1360 TextPosition? _textSelectionEnd;
1361
1362 bool _selectableContainsOriginWord = false;
1363
1364 LayerLink? _startHandleLayerLink;
1365 LayerLink? _endHandleLayerLink;
1366
1367 @override
1368 SelectionGeometry get value => _selectionGeometry;
1369 late SelectionGeometry _selectionGeometry;
1370 void _updateSelectionGeometry() {
1371 final SelectionGeometry newValue = _getSelectionGeometry();
1372 if (_selectionGeometry == newValue) {
1373 return;
1374 }
1375 _selectionGeometry = newValue;
1376 notifyListeners();
1377 }
1378
1379 SelectionGeometry _getSelectionGeometry() {
1380 if (_textSelectionStart == null || _textSelectionEnd == null) {
1381 return const SelectionGeometry(
1382 status: SelectionStatus.none,
1383 hasContent: true,
1384 );
1385 }
1386
1387 final int selectionStart = _textSelectionStart!.offset;
1388 final int selectionEnd = _textSelectionEnd!.offset;
1389 final bool isReversed = selectionStart > selectionEnd;
1390 final Offset startOffsetInParagraphCoordinates = paragraph._getOffsetForPosition(TextPosition(offset: selectionStart));
1391 final Offset endOffsetInParagraphCoordinates = selectionStart == selectionEnd
1392 ? startOffsetInParagraphCoordinates
1393 : paragraph._getOffsetForPosition(TextPosition(offset: selectionEnd));
1394 final bool flipHandles = isReversed != (TextDirection.rtl == paragraph.textDirection);
1395 final TextSelection selection = TextSelection(
1396 baseOffset: selectionStart,
1397 extentOffset: selectionEnd,
1398 );
1399 final List<Rect> selectionRects = <Rect>[];
1400 for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) {
1401 selectionRects.add(textBox.toRect());
1402 }
1403 return SelectionGeometry(
1404 startSelectionPoint: SelectionPoint(
1405 localPosition: startOffsetInParagraphCoordinates,
1406 lineHeight: paragraph._textPainter.preferredLineHeight,
1407 handleType: flipHandles ? TextSelectionHandleType.right : TextSelectionHandleType.left
1408 ),
1409 endSelectionPoint: SelectionPoint(
1410 localPosition: endOffsetInParagraphCoordinates,
1411 lineHeight: paragraph._textPainter.preferredLineHeight,
1412 handleType: flipHandles ? TextSelectionHandleType.left : TextSelectionHandleType.right,
1413 ),
1414 selectionRects: selectionRects,
1415 status: _textSelectionStart!.offset == _textSelectionEnd!.offset
1416 ? SelectionStatus.collapsed
1417 : SelectionStatus.uncollapsed,
1418 hasContent: true,
1419 );
1420 }
1421
1422 @override
1423 SelectionResult dispatchSelectionEvent(SelectionEvent event) {
1424 late final SelectionResult result;
1425 final TextPosition? existingSelectionStart = _textSelectionStart;
1426 final TextPosition? existingSelectionEnd = _textSelectionEnd;
1427 switch (event.type) {
1428 case SelectionEventType.startEdgeUpdate:
1429 case SelectionEventType.endEdgeUpdate:
1430 final SelectionEdgeUpdateEvent edgeUpdate = event as SelectionEdgeUpdateEvent;
1431 final TextGranularity granularity = event.granularity;
1432
1433 switch (granularity) {
1434 case TextGranularity.character:
1435 result = _updateSelectionEdge(edgeUpdate.globalPosition, isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate);
1436 case TextGranularity.word:
1437 result = _updateSelectionEdgeByWord(edgeUpdate.globalPosition, isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate);
1438 case TextGranularity.document:
1439 case TextGranularity.line:
1440 assert(false, 'Moving the selection edge by line or document is not supported.');
1441 }
1442 case SelectionEventType.clear:
1443 result = _handleClearSelection();
1444 case SelectionEventType.selectAll:
1445 result = _handleSelectAll();
1446 case SelectionEventType.selectWord:
1447 final SelectWordSelectionEvent selectWord = event as SelectWordSelectionEvent;
1448 result = _handleSelectWord(selectWord.globalPosition);
1449 case SelectionEventType.granularlyExtendSelection:
1450 final GranularlyExtendSelectionEvent granularlyExtendSelection = event as GranularlyExtendSelectionEvent;
1451 result = _handleGranularlyExtendSelection(
1452 granularlyExtendSelection.forward,
1453 granularlyExtendSelection.isEnd,
1454 granularlyExtendSelection.granularity,
1455 );
1456 case SelectionEventType.directionallyExtendSelection:
1457 final DirectionallyExtendSelectionEvent directionallyExtendSelection = event as DirectionallyExtendSelectionEvent;
1458 result = _handleDirectionallyExtendSelection(
1459 directionallyExtendSelection.dx,
1460 directionallyExtendSelection.isEnd,
1461 directionallyExtendSelection.direction,
1462 );
1463 }
1464
1465 if (existingSelectionStart != _textSelectionStart ||
1466 existingSelectionEnd != _textSelectionEnd) {
1467 _didChangeSelection();
1468 }
1469 return result;
1470 }
1471
1472 @override
1473 SelectedContent? getSelectedContent() {
1474 if (_textSelectionStart == null || _textSelectionEnd == null) {
1475 return null;
1476 }
1477 final int start = math.min(_textSelectionStart!.offset, _textSelectionEnd!.offset);
1478 final int end = math.max(_textSelectionStart!.offset, _textSelectionEnd!.offset);
1479 return SelectedContent(
1480 plainText: fullText.substring(start, end),
1481 );
1482 }
1483
1484 void _didChangeSelection() {
1485 paragraph.markNeedsPaint();
1486 _updateSelectionGeometry();
1487 }
1488
1489 SelectionResult _updateSelectionEdge(Offset globalPosition, {required bool isEnd}) {
1490 _setSelectionPosition(null, isEnd: isEnd);
1491 final Matrix4 transform = paragraph.getTransformTo(null);
1492 transform.invert();
1493 final Offset localPosition = MatrixUtils.transformPoint(transform, globalPosition);
1494 if (_rect.isEmpty) {
1495 return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
1496 }
1497 final Offset adjustedOffset = SelectionUtils.adjustDragOffset(
1498 _rect,
1499 localPosition,
1500 direction: paragraph.textDirection,
1501 );
1502
1503 final TextPosition position = _clampTextPosition(paragraph.getPositionForOffset(adjustedOffset));
1504 _setSelectionPosition(position, isEnd: isEnd);
1505 if (position.offset == range.end) {
1506 return SelectionResult.next;
1507 }
1508 if (position.offset == range.start) {
1509 return SelectionResult.previous;
1510 }
1511 // TODO(chunhtai): The geometry information should not be used to determine
1512 // selection result. This is a workaround to RenderParagraph, where it does
1513 // not have a way to get accurate text length if its text is truncated due to
1514 // layout constraint.
1515 return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
1516 }
1517
1518 TextPosition _closestWordBoundary(
1519 _WordBoundaryRecord wordBoundary,
1520 TextPosition position,
1521 ) {
1522 final int differenceA = (position.offset - wordBoundary.wordStart.offset).abs();
1523 final int differenceB = (position.offset - wordBoundary.wordEnd.offset).abs();
1524 return differenceA < differenceB ? wordBoundary.wordStart : wordBoundary.wordEnd;
1525 }
1526
1527 TextPosition _updateSelectionStartEdgeByWord(
1528 _WordBoundaryRecord? wordBoundary,
1529 TextPosition position,
1530 TextPosition? existingSelectionStart,
1531 TextPosition? existingSelectionEnd,
1532 ) {
1533 TextPosition? targetPosition;
1534 if (wordBoundary != null) {
1535 assert(wordBoundary.wordStart.offset >= range.start && wordBoundary.wordEnd.offset <= range.end);
1536 if (_selectableContainsOriginWord && existingSelectionStart != null && existingSelectionEnd != null) {
1537 final bool isSamePosition = position.offset == existingSelectionEnd.offset;
1538 final bool isSelectionInverted = existingSelectionStart.offset > existingSelectionEnd.offset;
1539 final bool shouldSwapEdges = !isSamePosition && (isSelectionInverted != (position.offset > existingSelectionEnd.offset));
1540 if (shouldSwapEdges) {
1541 if (position.offset < existingSelectionEnd.offset) {
1542 targetPosition = wordBoundary.wordStart;
1543 } else {
1544 targetPosition = wordBoundary.wordEnd;
1545 }
1546 // When the selection is inverted by the new position it is necessary to
1547 // swap the start edge (moving edge) with the end edge (static edge) to
1548 // maintain the origin word within the selection.
1549 final _WordBoundaryRecord localWordBoundary = _getWordBoundaryAtPosition(existingSelectionEnd);
1550 assert(localWordBoundary.wordStart.offset >= range.start && localWordBoundary.wordEnd.offset <= range.end);
1551 _setSelectionPosition(existingSelectionEnd.offset == localWordBoundary.wordStart.offset ? localWordBoundary.wordEnd : localWordBoundary.wordStart, isEnd: true);
1552 } else {
1553 if (position.offset < existingSelectionEnd.offset) {
1554 targetPosition = wordBoundary.wordStart;
1555 } else if (position.offset > existingSelectionEnd.offset) {
1556 targetPosition = wordBoundary.wordEnd;
1557 } else {
1558 // Keep the origin word in bounds when position is at the static edge.
1559 targetPosition = existingSelectionStart;
1560 }
1561 }
1562 } else {
1563 if (existingSelectionEnd != null) {
1564 // If the end edge exists and the start edge is being moved, then the
1565 // start edge is moved to encompass the entire word at the new position.
1566 if (position.offset < existingSelectionEnd.offset) {
1567 targetPosition = wordBoundary.wordStart;
1568 } else {
1569 targetPosition = wordBoundary.wordEnd;
1570 }
1571 } else {
1572 // Move the start edge to the closest word boundary.
1573 targetPosition = _closestWordBoundary(wordBoundary, position);
1574 }
1575 }
1576 } else {
1577 // The position is not contained within the current rect. The targetPosition
1578 // will either be at the end or beginning of the current rect. See [SelectionUtils.adjustDragOffset]
1579 // for a more in depth explanation on this adjustment.
1580 if (_selectableContainsOriginWord && existingSelectionStart != null && existingSelectionEnd != null) {
1581 // When the selection is inverted by the new position it is necessary to
1582 // swap the start edge (moving edge) with the end edge (static edge) to
1583 // maintain the origin word within the selection.
1584 final bool isSamePosition = position.offset == existingSelectionEnd.offset;
1585 final bool isSelectionInverted = existingSelectionStart.offset > existingSelectionEnd.offset;
1586 final bool shouldSwapEdges = !isSamePosition && (isSelectionInverted != (position.offset > existingSelectionEnd.offset));
1587
1588 if (shouldSwapEdges) {
1589 final _WordBoundaryRecord localWordBoundary = _getWordBoundaryAtPosition(existingSelectionEnd);
1590 assert(localWordBoundary.wordStart.offset >= range.start && localWordBoundary.wordEnd.offset <= range.end);
1591 _setSelectionPosition(isSelectionInverted ? localWordBoundary.wordEnd : localWordBoundary.wordStart, isEnd: true);
1592 }
1593 }
1594 }
1595 return targetPosition ?? position;
1596 }
1597
1598 TextPosition _updateSelectionEndEdgeByWord(
1599 _WordBoundaryRecord? wordBoundary,
1600 TextPosition position,
1601 TextPosition? existingSelectionStart,
1602 TextPosition? existingSelectionEnd,
1603 ) {
1604 TextPosition? targetPosition;
1605 if (wordBoundary != null) {
1606 assert(wordBoundary.wordStart.offset >= range.start && wordBoundary.wordEnd.offset <= range.end);
1607 if (_selectableContainsOriginWord && existingSelectionStart != null && existingSelectionEnd != null) {
1608 final bool isSamePosition = position.offset == existingSelectionStart.offset;
1609 final bool isSelectionInverted = existingSelectionStart.offset > existingSelectionEnd.offset;
1610 final bool shouldSwapEdges = !isSamePosition && (isSelectionInverted != (position.offset < existingSelectionStart.offset));
1611 if (shouldSwapEdges) {
1612 if (position.offset < existingSelectionStart.offset) {
1613 targetPosition = wordBoundary.wordStart;
1614 } else {
1615 targetPosition = wordBoundary.wordEnd;
1616 }
1617 // When the selection is inverted by the new position it is necessary to
1618 // swap the end edge (moving edge) with the start edge (static edge) to
1619 // maintain the origin word within the selection.
1620 final _WordBoundaryRecord localWordBoundary = _getWordBoundaryAtPosition(existingSelectionStart);
1621 assert(localWordBoundary.wordStart.offset >= range.start && localWordBoundary.wordEnd.offset <= range.end);
1622 _setSelectionPosition(existingSelectionStart.offset == localWordBoundary.wordStart.offset ? localWordBoundary.wordEnd : localWordBoundary.wordStart, isEnd: false);
1623 } else {
1624 if (position.offset < existingSelectionStart.offset) {
1625 targetPosition = wordBoundary.wordStart;
1626 } else if (position.offset > existingSelectionStart.offset) {
1627 targetPosition = wordBoundary.wordEnd;
1628 } else {
1629 // Keep the origin word in bounds when position is at the static edge.
1630 targetPosition = existingSelectionEnd;
1631 }
1632 }
1633 } else {
1634 if (existingSelectionStart != null) {
1635 // If the start edge exists and the end edge is being moved, then the
1636 // end edge is moved to encompass the entire word at the new position.
1637 if (position.offset < existingSelectionStart.offset) {
1638 targetPosition = wordBoundary.wordStart;
1639 } else {
1640 targetPosition = wordBoundary.wordEnd;
1641 }
1642 } else {
1643 // Move the end edge to the closest word boundary.
1644 targetPosition = _closestWordBoundary(wordBoundary, position);
1645 }
1646 }
1647 } else {
1648 // The position is not contained within the current rect. The targetPosition
1649 // will either be at the end or beginning of the current rect. See [SelectionUtils.adjustDragOffset]
1650 // for a more in depth explanation on this adjustment.
1651 if (_selectableContainsOriginWord && existingSelectionStart != null && existingSelectionEnd != null) {
1652 // When the selection is inverted by the new position it is necessary to
1653 // swap the end edge (moving edge) with the start edge (static edge) to
1654 // maintain the origin word within the selection.
1655 final bool isSamePosition = position.offset == existingSelectionStart.offset;
1656 final bool isSelectionInverted = existingSelectionStart.offset > existingSelectionEnd.offset;
1657 final bool shouldSwapEdges = isSelectionInverted != (position.offset < existingSelectionStart.offset) || isSamePosition;
1658 if (shouldSwapEdges) {
1659 final _WordBoundaryRecord localWordBoundary = _getWordBoundaryAtPosition(existingSelectionStart);
1660 assert(localWordBoundary.wordStart.offset >= range.start && localWordBoundary.wordEnd.offset <= range.end);
1661 _setSelectionPosition(isSelectionInverted ? localWordBoundary.wordStart : localWordBoundary.wordEnd, isEnd: false);
1662 }
1663 }
1664 }
1665 return targetPosition ?? position;
1666 }
1667
1668 SelectionResult _updateSelectionEdgeByWord(Offset globalPosition, {required bool isEnd}) {
1669 // When the start/end edges are swapped, i.e. the start is after the end, and
1670 // the scrollable synthesizes an event for the opposite edge, this will potentially
1671 // move the opposite edge outside of the origin word boundary and we are unable to recover.
1672 final TextPosition? existingSelectionStart = _textSelectionStart;
1673 final TextPosition? existingSelectionEnd = _textSelectionEnd;
1674
1675 _setSelectionPosition(null, isEnd: isEnd);
1676 final Matrix4 transform = paragraph.getTransformTo(null);
1677 transform.invert();
1678 final Offset localPosition = MatrixUtils.transformPoint(transform, globalPosition);
1679 if (_rect.isEmpty) {
1680 return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
1681 }
1682 final Offset adjustedOffset = SelectionUtils.adjustDragOffset(
1683 _rect,
1684 localPosition,
1685 direction: paragraph.textDirection,
1686 );
1687
1688 final TextPosition position = paragraph.getPositionForOffset(adjustedOffset);
1689 // Check if the original local position is within the rect, if it is not then
1690 // we do not need to look up the word boundary for that position. This is to
1691 // maintain a selectables selection collapsed at 0 when the local position is
1692 // not located inside its rect.
1693 _WordBoundaryRecord? wordBoundary = _rect.contains(localPosition) ? _getWordBoundaryAtPosition(position) : null;
1694 if (wordBoundary != null
1695 && (wordBoundary.wordStart.offset < range.start && wordBoundary.wordEnd.offset <= range.start
1696 || wordBoundary.wordStart.offset >= range.end && wordBoundary.wordEnd.offset > range.end)) {
1697 // When the position is located at a placeholder inside of the text, then we may compute
1698 // a word boundary that does not belong to the current selectable fragment. In this case
1699 // we should invalidate the word boundary so that it is not taken into account when
1700 // computing the target position.
1701 wordBoundary = null;
1702 }
1703 final TextPosition targetPosition = _clampTextPosition(isEnd ? _updateSelectionEndEdgeByWord(wordBoundary, position, existingSelectionStart, existingSelectionEnd) : _updateSelectionStartEdgeByWord(wordBoundary, position, existingSelectionStart, existingSelectionEnd));
1704
1705 _setSelectionPosition(targetPosition, isEnd: isEnd);
1706 if (targetPosition.offset == range.end) {
1707 return SelectionResult.next;
1708 }
1709
1710 if (targetPosition.offset == range.start) {
1711 return SelectionResult.previous;
1712 }
1713 // TODO(chunhtai): The geometry information should not be used to determine
1714 // selection result. This is a workaround to RenderParagraph, where it does
1715 // not have a way to get accurate text length if its text is truncated due to
1716 // layout constraint.
1717 return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
1718 }
1719
1720 TextPosition _clampTextPosition(TextPosition position) {
1721 // Affinity of range.end is upstream.
1722 if (position.offset > range.end ||
1723 (position.offset == range.end && position.affinity == TextAffinity.downstream)) {
1724 return TextPosition(offset: range.end, affinity: TextAffinity.upstream);
1725 }
1726 if (position.offset < range.start) {
1727 return TextPosition(offset: range.start);
1728 }
1729 return position;
1730 }
1731
1732 void _setSelectionPosition(TextPosition? position, {required bool isEnd}) {
1733 if (isEnd) {
1734 _textSelectionEnd = position;
1735 } else {
1736 _textSelectionStart = position;
1737 }
1738 }
1739
1740 SelectionResult _handleClearSelection() {
1741 _textSelectionStart = null;
1742 _textSelectionEnd = null;
1743 _selectableContainsOriginWord = false;
1744 return SelectionResult.none;
1745 }
1746
1747 SelectionResult _handleSelectAll() {
1748 _textSelectionStart = TextPosition(offset: range.start);
1749 _textSelectionEnd = TextPosition(offset: range.end, affinity: TextAffinity.upstream);
1750 return SelectionResult.none;
1751 }
1752
1753 SelectionResult _handleSelectWord(Offset globalPosition) {
1754 final TextPosition position = paragraph.getPositionForOffset(paragraph.globalToLocal(globalPosition));
1755 if (_positionIsWithinCurrentSelection(position) && _textSelectionStart != _textSelectionEnd) {
1756 return SelectionResult.end;
1757 }
1758 final _WordBoundaryRecord wordBoundary = _getWordBoundaryAtPosition(position);
1759 // This fragment may not contain the word, decide what direction the target
1760 // fragment is located in. Because fragments are separated by placeholder
1761 // spans, we also check if the beginning or end of the word is touching
1762 // either edge of this fragment.
1763 if (wordBoundary.wordStart.offset < range.start && wordBoundary.wordEnd.offset <= range.start) {
1764 return SelectionResult.previous;
1765 } else if (wordBoundary.wordStart.offset >= range.end && wordBoundary.wordEnd.offset > range.end) {
1766 return SelectionResult.next;
1767 }
1768 // Fragments are separated by placeholder span, the word boundary shouldn't
1769 // expand across fragments.
1770 assert(wordBoundary.wordStart.offset >= range.start && wordBoundary.wordEnd.offset <= range.end);
1771 _textSelectionStart = wordBoundary.wordStart;
1772 _textSelectionEnd = wordBoundary.wordEnd;
1773 _selectableContainsOriginWord = true;
1774 return SelectionResult.end;
1775 }
1776
1777 _WordBoundaryRecord _getWordBoundaryAtPosition(TextPosition position) {
1778 final TextRange word = paragraph.getWordBoundary(position);
1779 assert(word.isNormalized);
1780 late TextPosition start;
1781 late TextPosition end;
1782 if (position.offset > word.end) {
1783 start = end = TextPosition(offset: position.offset);
1784 } else {
1785 start = TextPosition(offset: word.start);
1786 end = TextPosition(offset: word.end, affinity: TextAffinity.upstream);
1787 }
1788 return (wordStart: start, wordEnd: end);
1789 }
1790
1791 SelectionResult _handleDirectionallyExtendSelection(double horizontalBaseline, bool isExtent, SelectionExtendDirection movement) {
1792 final Matrix4 transform = paragraph.getTransformTo(null);
1793 if (transform.invert() == 0.0) {
1794 switch (movement) {
1795 case SelectionExtendDirection.previousLine:
1796 case SelectionExtendDirection.backward:
1797 return SelectionResult.previous;
1798 case SelectionExtendDirection.nextLine:
1799 case SelectionExtendDirection.forward:
1800 return SelectionResult.next;
1801 }
1802 }
1803 final double baselineInParagraphCoordinates = MatrixUtils.transformPoint(transform, Offset(horizontalBaseline, 0)).dx;
1804 assert(!baselineInParagraphCoordinates.isNaN);
1805 final TextPosition newPosition;
1806 final SelectionResult result;
1807 switch (movement) {
1808 case SelectionExtendDirection.previousLine:
1809 case SelectionExtendDirection.nextLine:
1810 assert(_textSelectionEnd != null && _textSelectionStart != null);
1811 final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart!;
1812 final MapEntry<TextPosition, SelectionResult> moveResult = _handleVerticalMovement(
1813 targetedEdge,
1814 horizontalBaselineInParagraphCoordinates: baselineInParagraphCoordinates,
1815 below: movement == SelectionExtendDirection.nextLine,
1816 );
1817 newPosition = moveResult.key;
1818 result = moveResult.value;
1819 case SelectionExtendDirection.forward:
1820 case SelectionExtendDirection.backward:
1821 _textSelectionEnd ??= movement == SelectionExtendDirection.forward
1822 ? TextPosition(offset: range.start)
1823 : TextPosition(offset: range.end, affinity: TextAffinity.upstream);
1824 _textSelectionStart ??= _textSelectionEnd;
1825 final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart!;
1826 final Offset edgeOffsetInParagraphCoordinates = paragraph._getOffsetForPosition(targetedEdge);
1827 final Offset baselineOffsetInParagraphCoordinates = Offset(
1828 baselineInParagraphCoordinates,
1829 // Use half of line height to point to the middle of the line.
1830 edgeOffsetInParagraphCoordinates.dy - paragraph._textPainter.preferredLineHeight / 2,
1831 );
1832 newPosition = paragraph.getPositionForOffset(baselineOffsetInParagraphCoordinates);
1833 result = SelectionResult.end;
1834 }
1835 if (isExtent) {
1836 _textSelectionEnd = newPosition;
1837 } else {
1838 _textSelectionStart = newPosition;
1839 }
1840 return result;
1841 }
1842
1843 SelectionResult _handleGranularlyExtendSelection(bool forward, bool isExtent, TextGranularity granularity) {
1844 _textSelectionEnd ??= forward
1845 ? TextPosition(offset: range.start)
1846 : TextPosition(offset: range.end, affinity: TextAffinity.upstream);
1847 _textSelectionStart ??= _textSelectionEnd;
1848 final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart!;
1849 if (forward && (targetedEdge.offset == range.end)) {
1850 return SelectionResult.next;
1851 }
1852 if (!forward && (targetedEdge.offset == range.start)) {
1853 return SelectionResult.previous;
1854 }
1855 final SelectionResult result;
1856 final TextPosition newPosition;
1857 switch (granularity) {
1858 case TextGranularity.character:
1859 final String text = range.textInside(fullText);
1860 newPosition = _moveBeyondTextBoundaryAtDirection(targetedEdge, forward, CharacterBoundary(text));
1861 result = SelectionResult.end;
1862 case TextGranularity.word:
1863 final TextBoundary textBoundary = paragraph._textPainter.wordBoundaries.moveByWordBoundary;
1864 newPosition = _moveBeyondTextBoundaryAtDirection(targetedEdge, forward, textBoundary);
1865 result = SelectionResult.end;
1866 case TextGranularity.line:
1867 newPosition = _moveToTextBoundaryAtDirection(targetedEdge, forward, LineBoundary(this));
1868 result = SelectionResult.end;
1869 case TextGranularity.document:
1870 final String text = range.textInside(fullText);
1871 newPosition = _moveBeyondTextBoundaryAtDirection(targetedEdge, forward, DocumentBoundary(text));
1872 if (forward && newPosition.offset == range.end) {
1873 result = SelectionResult.next;
1874 } else if (!forward && newPosition.offset == range.start) {
1875 result = SelectionResult.previous;
1876 } else {
1877 result = SelectionResult.end;
1878 }
1879 }
1880
1881 if (isExtent) {
1882 _textSelectionEnd = newPosition;
1883 } else {
1884 _textSelectionStart = newPosition;
1885 }
1886 return result;
1887 }
1888
1889 // Move **beyond** the local boundary of the given type (unless range.start or
1890 // range.end is reached). Used for most TextGranularity types except for
1891 // TextGranularity.line, to ensure the selection movement doesn't get stuck at
1892 // a local fixed point.
1893 TextPosition _moveBeyondTextBoundaryAtDirection(TextPosition end, bool forward, TextBoundary textBoundary) {
1894 final int newOffset = forward
1895 ? textBoundary.getTrailingTextBoundaryAt(end.offset) ?? range.end
1896 : textBoundary.getLeadingTextBoundaryAt(end.offset - 1) ?? range.start;
1897 return TextPosition(offset: newOffset);
1898 }
1899
1900 // Move **to** the local boundary of the given type. Typically used for line
1901 // boundaries, such that performing "move to line start" more than once never
1902 // moves the selection to the previous line.
1903 TextPosition _moveToTextBoundaryAtDirection(TextPosition end, bool forward, TextBoundary textBoundary) {
1904 assert(end.offset >= 0);
1905 final int caretOffset;
1906 switch (end.affinity) {
1907 case TextAffinity.upstream:
1908 if (end.offset < 1 && !forward) {
1909 assert (end.offset == 0);
1910 return const TextPosition(offset: 0);
1911 }
1912 final CharacterBoundary characterBoundary = CharacterBoundary(fullText);
1913 caretOffset = math.max(
1914 0,
1915 characterBoundary.getLeadingTextBoundaryAt(range.start + end.offset) ?? range.start,
1916 ) - 1;
1917 case TextAffinity.downstream:
1918 caretOffset = end.offset;
1919 }
1920 final int offset = forward
1921 ? textBoundary.getTrailingTextBoundaryAt(caretOffset) ?? range.end
1922 : textBoundary.getLeadingTextBoundaryAt(caretOffset) ?? range.start;
1923 return TextPosition(offset: offset);
1924 }
1925
1926 MapEntry<TextPosition, SelectionResult> _handleVerticalMovement(TextPosition position, {required double horizontalBaselineInParagraphCoordinates, required bool below}) {
1927 final List<ui.LineMetrics> lines = paragraph._computeLineMetrics();
1928 final Offset offset = paragraph.getOffsetForCaret(position, Rect.zero);
1929 int currentLine = lines.length - 1;
1930 for (final ui.LineMetrics lineMetrics in lines) {
1931 if (lineMetrics.baseline > offset.dy) {
1932 currentLine = lineMetrics.lineNumber;
1933 break;
1934 }
1935 }
1936 final TextPosition newPosition;
1937 if (below && currentLine == lines.length - 1) {
1938 newPosition = TextPosition(offset: range.end, affinity: TextAffinity.upstream);
1939 } else if (!below && currentLine == 0) {
1940 newPosition = TextPosition(offset: range.start);
1941 } else {
1942 final int newLine = below ? currentLine + 1 : currentLine - 1;
1943 newPosition = _clampTextPosition(
1944 paragraph.getPositionForOffset(Offset(horizontalBaselineInParagraphCoordinates, lines[newLine].baseline))
1945 );
1946 }
1947 final SelectionResult result;
1948 if (newPosition.offset == range.start) {
1949 result = SelectionResult.previous;
1950 } else if (newPosition.offset == range.end) {
1951 result = SelectionResult.next;
1952 } else {
1953 result = SelectionResult.end;
1954 }
1955 assert(result != SelectionResult.next || below);
1956 assert(result != SelectionResult.previous || !below);
1957 return MapEntry<TextPosition, SelectionResult>(newPosition, result);
1958 }
1959
1960 /// Whether the given text position is contained in current selection
1961 /// range.
1962 ///
1963 /// The parameter `start` must be smaller than `end`.
1964 bool _positionIsWithinCurrentSelection(TextPosition position) {
1965 if (_textSelectionStart == null || _textSelectionEnd == null) {
1966 return false;
1967 }
1968 // Normalize current selection.
1969 late TextPosition currentStart;
1970 late TextPosition currentEnd;
1971 if (_compareTextPositions(_textSelectionStart!, _textSelectionEnd!) > 0) {
1972 currentStart = _textSelectionStart!;
1973 currentEnd = _textSelectionEnd!;
1974 } else {
1975 currentStart = _textSelectionEnd!;
1976 currentEnd = _textSelectionStart!;
1977 }
1978 return _compareTextPositions(currentStart, position) >= 0 && _compareTextPositions(currentEnd, position) <= 0;
1979 }
1980
1981 /// Compares two text positions.
1982 ///
1983 /// Returns 1 if `position` < `otherPosition`, -1 if `position` > `otherPosition`,
1984 /// or 0 if they are equal.
1985 static int _compareTextPositions(TextPosition position, TextPosition otherPosition) {
1986 if (position.offset < otherPosition.offset) {
1987 return 1;
1988 } else if (position.offset > otherPosition.offset) {
1989 return -1;
1990 } else if (position.affinity == otherPosition.affinity){
1991 return 0;
1992 } else {
1993 return position.affinity == TextAffinity.upstream ? 1 : -1;
1994 }
1995 }
1996
1997 @override
1998 Matrix4 getTransformTo(RenderObject? ancestor) {
1999 return paragraph.getTransformTo(ancestor);
2000 }
2001
2002 @override
2003 void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
2004 if (!paragraph.attached) {
2005 assert(startHandle == null && endHandle == null, 'Only clean up can be called.');
2006 return;
2007 }
2008 if (_startHandleLayerLink != startHandle) {
2009 _startHandleLayerLink = startHandle;
2010 paragraph.markNeedsPaint();
2011 }
2012 if (_endHandleLayerLink != endHandle) {
2013 _endHandleLayerLink = endHandle;
2014 paragraph.markNeedsPaint();
2015 }
2016 }
2017
2018 List<Rect>? _cachedBoundingBoxes;
2019 @override
2020 List<Rect> get boundingBoxes {
2021 if (_cachedBoundingBoxes == null) {
2022 final List<TextBox> boxes = paragraph.getBoxesForSelection(
2023 TextSelection(baseOffset: range.start, extentOffset: range.end),
2024 );
2025 if (boxes.isNotEmpty) {
2026 _cachedBoundingBoxes = <Rect>[];
2027 for (final TextBox textBox in boxes) {
2028 _cachedBoundingBoxes!.add(textBox.toRect());
2029 }
2030 } else {
2031 final Offset offset = paragraph._getOffsetForPosition(TextPosition(offset: range.start));
2032 final Rect rect = Rect.fromPoints(offset, offset.translate(0, - paragraph._textPainter.preferredLineHeight));
2033 _cachedBoundingBoxes = <Rect>[rect];
2034 }
2035 }
2036 return _cachedBoundingBoxes!;
2037 }
2038
2039 Rect? _cachedRect;
2040 Rect get _rect {
2041 if (_cachedRect == null) {
2042 final List<TextBox> boxes = paragraph.getBoxesForSelection(
2043 TextSelection(baseOffset: range.start, extentOffset: range.end),
2044 );
2045 if (boxes.isNotEmpty) {
2046 Rect result = boxes.first.toRect();
2047 for (int index = 1; index < boxes.length; index += 1) {
2048 result = result.expandToInclude(boxes[index].toRect());
2049 }
2050 _cachedRect = result;
2051 } else {
2052 final Offset offset = paragraph._getOffsetForPosition(TextPosition(offset: range.start));
2053 _cachedRect = Rect.fromPoints(offset, offset.translate(0, - paragraph._textPainter.preferredLineHeight));
2054 }
2055 }
2056 return _cachedRect!;
2057 }
2058
2059 void didChangeParagraphLayout() {
2060 _cachedRect = null;
2061 }
2062
2063 @override
2064 Size get size {
2065 return _rect.size;
2066 }
2067
2068 void paint(PaintingContext context, Offset offset) {
2069 if (_textSelectionStart == null || _textSelectionEnd == null) {
2070 return;
2071 }
2072 if (paragraph.selectionColor != null) {
2073 final TextSelection selection = TextSelection(
2074 baseOffset: _textSelectionStart!.offset,
2075 extentOffset: _textSelectionEnd!.offset,
2076 );
2077 final Paint selectionPaint = Paint()
2078 ..style = PaintingStyle.fill
2079 ..color = paragraph.selectionColor!;
2080 for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) {
2081 context.canvas.drawRect(
2082 textBox.toRect().shift(offset), selectionPaint);
2083 }
2084 }
2085 if (_startHandleLayerLink != null && value.startSelectionPoint != null) {
2086 context.pushLayer(
2087 LeaderLayer(
2088 link: _startHandleLayerLink!,
2089 offset: offset + value.startSelectionPoint!.localPosition,
2090 ),
2091 (PaintingContext context, Offset offset) { },
2092 Offset.zero,
2093 );
2094 }
2095 if (_endHandleLayerLink != null && value.endSelectionPoint != null) {
2096 context.pushLayer(
2097 LeaderLayer(
2098 link: _endHandleLayerLink!,
2099 offset: offset + value.endSelectionPoint!.localPosition,
2100 ),
2101 (PaintingContext context, Offset offset) { },
2102 Offset.zero,
2103 );
2104 }
2105 }
2106
2107 @override
2108 TextSelection getLineAtOffset(TextPosition position) {
2109 final TextRange line = paragraph._getLineAtOffset(position);
2110 final int start = line.start.clamp(range.start, range.end);
2111 final int end = line.end.clamp(range.start, range.end);
2112 return TextSelection(baseOffset: start, extentOffset: end);
2113 }
2114
2115 @override
2116 TextPosition getTextPositionAbove(TextPosition position) {
2117 return _clampTextPosition(paragraph._getTextPositionAbove(position));
2118 }
2119
2120 @override
2121 TextPosition getTextPositionBelow(TextPosition position) {
2122 return _clampTextPosition(paragraph._getTextPositionBelow(position));
2123 }
2124
2125 @override
2126 TextRange getWordBoundary(TextPosition position) => paragraph.getWordBoundary(position);
2127
2128 @override
2129 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
2130 super.debugFillProperties(properties);
2131 properties.add(DiagnosticsProperty<String>('textInsideRange', range.textInside(fullText)));
2132 properties.add(DiagnosticsProperty<TextRange>('range', range));
2133 properties.add(DiagnosticsProperty<String>('fullText', fullText));
2134 }
2135}
2136