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
5/// @docImport 'package:flutter/widgets.dart';
6///
7/// @docImport 'editable.dart';
8library;
9
10import 'dart:math' as math;
11import 'dart:ui'
12 as ui
13 show
14 BoxHeightStyle,
15 BoxWidthStyle,
16 Gradient,
17 LineMetrics,
18 PlaceholderAlignment,
19 Shader,
20 TextBox,
21 TextHeightBehavior;
22
23import 'package:flutter/foundation.dart';
24import 'package:flutter/gestures.dart';
25import 'package:flutter/semantics.dart';
26import 'package:flutter/services.dart';
27
28import 'box.dart';
29import 'debug.dart';
30import 'layer.dart';
31import 'layout_helper.dart';
32import 'object.dart';
33import 'selection.dart';
34
35/// The start and end positions for a text boundary.
36typedef _TextBoundaryRecord = ({TextPosition boundaryStart, TextPosition boundaryEnd});
37
38/// Signature for a function that determines the [_TextBoundaryRecord] at the given
39/// [TextPosition].
40typedef _TextBoundaryAtPosition = _TextBoundaryRecord Function(TextPosition position);
41
42/// Signature for a function that determines the [_TextBoundaryRecord] at the given
43/// [TextPosition], for the given [String].
44typedef _TextBoundaryAtPositionInText =
45 _TextBoundaryRecord Function(TextPosition position, String text);
46
47const String _kEllipsis = '\u2026';
48
49/// Used by the [RenderParagraph] to map its rendering children to their
50/// corresponding semantics nodes.
51///
52/// The [RichText] uses this to tag the relation between its placeholder spans
53/// and their semantics nodes.
54@immutable
55class PlaceholderSpanIndexSemanticsTag extends SemanticsTag {
56 /// Creates a semantics tag with the input `index`.
57 ///
58 /// Different [PlaceholderSpanIndexSemanticsTag]s with the same `index` are
59 /// consider the same.
60 const PlaceholderSpanIndexSemanticsTag(this.index)
61 : super('PlaceholderSpanIndexSemanticsTag($index)');
62
63 /// The index of this tag.
64 final int index;
65
66 @override
67 bool operator ==(Object other) {
68 return other is PlaceholderSpanIndexSemanticsTag && other.index == index;
69 }
70
71 @override
72 int get hashCode => Object.hash(PlaceholderSpanIndexSemanticsTag, index);
73}
74
75/// Parent data used by [RenderParagraph] and [RenderEditable] to annotate
76/// inline contents (such as [WidgetSpan]s) with.
77class TextParentData extends ParentData with ContainerParentDataMixin<RenderBox> {
78 /// The offset at which to paint the child in the parent's coordinate system.
79 ///
80 /// A `null` value indicates this inline widget is not laid out. For instance,
81 /// when the inline widget has never been laid out, or the inline widget is
82 /// ellipsized away.
83 Offset? get offset => _offset;
84 Offset? _offset;
85
86 /// The [PlaceholderSpan] associated with this render child.
87 ///
88 /// This field is usually set by a [ParentDataWidget], and is typically not
89 /// null when `performLayout` is called.
90 PlaceholderSpan? span;
91
92 @override
93 void detach() {
94 span = null;
95 _offset = null;
96 super.detach();
97 }
98
99 @override
100 String toString() => 'widget: $span, ${offset == null ? "not laid out" : "offset: $offset"}';
101}
102
103/// A mixin that provides useful default behaviors for text [RenderBox]es
104/// ([RenderParagraph] and [RenderEditable] for example) with inline content
105/// children managed by the [ContainerRenderObjectMixin] mixin.
106///
107/// This mixin assumes every child managed by the [ContainerRenderObjectMixin]
108/// mixin corresponds to a [PlaceholderSpan], and they are organized in logical
109/// order of the text (the order each [PlaceholderSpan] is encountered when the
110/// user reads the text).
111///
112/// To use this mixin in a [RenderBox] class:
113///
114/// * Call [layoutInlineChildren] in the `performLayout` and `computeDryLayout`
115/// implementation, and during intrinsic size calculations, to get the size
116/// information of the inline widgets as a `List` of `PlaceholderDimensions`.
117/// Determine the positioning of the inline widgets (which is usually done by
118/// a [TextPainter] using its line break algorithm).
119///
120/// * Call [positionInlineChildren] with the positioning information of the
121/// inline widgets.
122///
123/// * Implement [RenderBox.applyPaintTransform], optionally with
124/// [defaultApplyPaintTransform].
125///
126/// * Call [paintInlineChildren] in [RenderBox.paint] to paint the inline widgets.
127///
128/// * Call [hitTestInlineChildren] in [RenderBox.hitTestChildren] to hit test the
129/// inline widgets.
130///
131/// See also:
132///
133/// * [WidgetSpan.extractFromInlineSpan], a helper function for extracting
134/// [WidgetSpan]s from an [InlineSpan] tree.
135mixin RenderInlineChildrenContainerDefaults
136 on RenderBox, ContainerRenderObjectMixin<RenderBox, TextParentData> {
137 @override
138 void setupParentData(RenderBox child) {
139 if (child.parentData is! TextParentData) {
140 child.parentData = TextParentData();
141 }
142 }
143
144 static PlaceholderDimensions _layoutChild(
145 RenderBox child,
146 BoxConstraints childConstraints,
147 ChildLayouter layoutChild,
148 ChildBaselineGetter getBaseline,
149 ) {
150 final TextParentData parentData = child.parentData! as TextParentData;
151 final PlaceholderSpan? span = parentData.span;
152 assert(span != null);
153 return span == null
154 ? PlaceholderDimensions.empty
155 : PlaceholderDimensions(
156 size: layoutChild(child, childConstraints),
157 alignment: span.alignment,
158 baseline: span.baseline,
159 baselineOffset: switch (span.alignment) {
160 ui.PlaceholderAlignment.aboveBaseline ||
161 ui.PlaceholderAlignment.belowBaseline ||
162 ui.PlaceholderAlignment.bottom ||
163 ui.PlaceholderAlignment.middle ||
164 ui.PlaceholderAlignment.top => null,
165 ui.PlaceholderAlignment.baseline => getBaseline(
166 child,
167 childConstraints,
168 span.baseline!,
169 ),
170 },
171 );
172 }
173
174 /// Computes the layout for every inline child using the `maxWidth` constraint.
175 ///
176 /// Returns a list of [PlaceholderDimensions], representing the layout results
177 /// for each child managed by the [ContainerRenderObjectMixin] mixin.
178 ///
179 /// The `getChildBaseline` parameter and the `layoutChild` parameter must be
180 /// consistent: if `layoutChild` computes the size of the child without
181 /// modifying the actual layout of that child, then `getChildBaseline` must
182 /// also be "dry", and vice versa.
183 ///
184 /// Since this method does not impose a maximum height constraint on the
185 /// inline children, some children may become taller than this [RenderBox].
186 ///
187 /// See also:
188 ///
189 /// * [TextPainter.setPlaceholderDimensions], the method that usually takes
190 /// the layout results from this method as the input.
191 @protected
192 List<PlaceholderDimensions> layoutInlineChildren(
193 double maxWidth,
194 ChildLayouter layoutChild,
195 ChildBaselineGetter getChildBaseline,
196 ) {
197 final BoxConstraints constraints = BoxConstraints(maxWidth: maxWidth);
198 return <PlaceholderDimensions>[
199 for (RenderBox? child = firstChild; child != null; child = childAfter(child))
200 _layoutChild(child, constraints, layoutChild, getChildBaseline),
201 ];
202 }
203
204 /// Positions each inline child according to the coordinates provided in the
205 /// `boxes` list.
206 ///
207 /// The `boxes` list must be in logical order, which is the order each child
208 /// is encountered when the user reads the text. Usually the length of the
209 /// list equals [childCount], but it can be less than that, when some children
210 /// are omitted due to ellipsing. It never exceeds [childCount].
211 ///
212 /// See also:
213 ///
214 /// * [TextPainter.inlinePlaceholderBoxes], the method that can be used to
215 /// get the input `boxes`.
216 @protected
217 void positionInlineChildren(List<ui.TextBox> boxes) {
218 RenderBox? child = firstChild;
219 for (final ui.TextBox box in boxes) {
220 if (child == null) {
221 assert(
222 false,
223 'The length of boxes (${boxes.length}) should be greater than childCount ($childCount)',
224 );
225 return;
226 }
227 final TextParentData textParentData = child.parentData! as TextParentData;
228 textParentData._offset = Offset(box.left, box.top);
229 child = childAfter(child);
230 }
231 while (child != null) {
232 final TextParentData textParentData = child.parentData! as TextParentData;
233 textParentData._offset = null;
234 child = childAfter(child);
235 }
236 }
237
238 /// Applies the transform that would be applied when painting the given child
239 /// to the given matrix.
240 ///
241 /// Render children whose [TextParentData.offset] is null zeros out the
242 /// `transform` to indicate they're invisible thus should not be painted.
243 @protected
244 void defaultApplyPaintTransform(RenderBox child, Matrix4 transform) {
245 final TextParentData childParentData = child.parentData! as TextParentData;
246 final Offset? offset = childParentData.offset;
247 if (offset == null) {
248 transform.setZero();
249 } else {
250 transform.translateByDouble(offset.dx, offset.dy, 0, 1);
251 }
252 }
253
254 /// Paints each inline child.
255 ///
256 /// Render children whose [TextParentData.offset] is null will be skipped by
257 /// this method.
258 @protected
259 void paintInlineChildren(PaintingContext context, Offset offset) {
260 RenderBox? child = firstChild;
261 while (child != null) {
262 final TextParentData childParentData = child.parentData! as TextParentData;
263 final Offset? childOffset = childParentData.offset;
264 if (childOffset == null) {
265 return;
266 }
267 context.paintChild(child, childOffset + offset);
268 child = childAfter(child);
269 }
270 }
271
272 /// Performs a hit test on each inline child.
273 ///
274 /// Render children whose [TextParentData.offset] is null will be skipped by
275 /// this method.
276 @protected
277 bool hitTestInlineChildren(BoxHitTestResult result, Offset position) {
278 RenderBox? child = firstChild;
279 while (child != null) {
280 final TextParentData childParentData = child.parentData! as TextParentData;
281 final Offset? childOffset = childParentData.offset;
282 if (childOffset == null) {
283 return false;
284 }
285 final bool isHit = result.addWithPaintOffset(
286 offset: childOffset,
287 position: position,
288 hitTest: (BoxHitTestResult result, Offset transformed) =>
289 child!.hitTest(result, position: transformed),
290 );
291 if (isHit) {
292 return true;
293 }
294 child = childAfter(child);
295 }
296 return false;
297 }
298}
299
300class _UnspecifiedTextScaler extends TextScaler {
301 const _UnspecifiedTextScaler();
302 @override
303 Never get textScaleFactor => throw UnimplementedError();
304
305 @override
306 Never scale(double fontSize) => throw UnimplementedError();
307}
308
309/// A render object that displays a paragraph of text.
310class RenderParagraph extends RenderBox
311 with
312 ContainerRenderObjectMixin<RenderBox, TextParentData>,
313 RenderInlineChildrenContainerDefaults,
314 RelayoutWhenSystemFontsChangeMixin {
315 /// Creates a paragraph render object.
316 ///
317 /// The [maxLines] property may be null (and indeed defaults to null), but if
318 /// it is not null, it must be greater than zero.
319 RenderParagraph(
320 InlineSpan text, {
321 TextAlign textAlign = TextAlign.start,
322 required TextDirection textDirection,
323 bool softWrap = true,
324 TextOverflow overflow = TextOverflow.clip,
325 @Deprecated(
326 'Use textScaler instead. '
327 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
328 'This feature was deprecated after v3.12.0-2.0.pre.',
329 )
330 double textScaleFactor = 1.0,
331 TextScaler textScaler = const _UnspecifiedTextScaler(),
332 int? maxLines,
333 Locale? locale,
334 StrutStyle? strutStyle,
335 TextWidthBasis textWidthBasis = TextWidthBasis.parent,
336 ui.TextHeightBehavior? textHeightBehavior,
337 List<RenderBox>? children,
338 Color? selectionColor,
339 SelectionRegistrar? registrar,
340 }) : assert(text.debugAssertIsValid()),
341 assert(maxLines == null || maxLines > 0),
342 assert(
343 identical(textScaler, const _UnspecifiedTextScaler()) || textScaleFactor == 1.0,
344 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
345 ),
346 _softWrap = softWrap,
347 _overflow = overflow,
348 _selectionColor = selectionColor,
349 _textPainter = TextPainter(
350 text: text,
351 textAlign: textAlign,
352 textDirection: textDirection,
353 textScaler: textScaler == const _UnspecifiedTextScaler()
354 ? TextScaler.linear(textScaleFactor)
355 : textScaler,
356 maxLines: maxLines,
357 ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
358 locale: locale,
359 strutStyle: strutStyle,
360 textWidthBasis: textWidthBasis,
361 textHeightBehavior: textHeightBehavior,
362 ) {
363 addAll(children);
364 this.registrar = registrar;
365 }
366
367 static final String _placeholderCharacter = String.fromCharCode(
368 PlaceholderSpan.placeholderCodeUnit,
369 );
370
371 final TextPainter _textPainter;
372
373 // Currently, computing min/max intrinsic width/height will destroy state
374 // inside the painter. Instead of calling _layout again to get back the correct
375 // state, use a separate TextPainter for intrinsics calculation.
376 //
377 // TODO(abarth): Make computing the min/max intrinsic width/height a
378 // non-destructive operation.
379 TextPainter? _textIntrinsicsCache;
380 TextPainter get _textIntrinsics {
381 return (_textIntrinsicsCache ??= TextPainter())
382 ..text = _textPainter.text
383 ..textAlign = _textPainter.textAlign
384 ..textDirection = _textPainter.textDirection
385 ..textScaler = _textPainter.textScaler
386 ..maxLines = _textPainter.maxLines
387 ..ellipsis = _textPainter.ellipsis
388 ..locale = _textPainter.locale
389 ..strutStyle = _textPainter.strutStyle
390 ..textWidthBasis = _textPainter.textWidthBasis
391 ..textHeightBehavior = _textPainter.textHeightBehavior;
392 }
393
394 List<AttributedString>? _cachedAttributedLabels;
395
396 List<InlineSpanSemanticsInformation>? _cachedCombinedSemanticsInfos;
397
398 /// The text to display.
399 InlineSpan get text => _textPainter.text!;
400 set text(InlineSpan value) {
401 switch (_textPainter.text!.compareTo(value)) {
402 case RenderComparison.identical:
403 return;
404 case RenderComparison.metadata:
405 _textPainter.text = value;
406 _cachedCombinedSemanticsInfos = null;
407 markNeedsSemanticsUpdate();
408 case RenderComparison.paint:
409 _textPainter.text = value;
410 _cachedAttributedLabels = null;
411 _cachedCombinedSemanticsInfos = null;
412 markNeedsPaint();
413 markNeedsSemanticsUpdate();
414 case RenderComparison.layout:
415 _textPainter.text = value;
416 _overflowShader = null;
417 _cachedAttributedLabels = null;
418 _cachedCombinedSemanticsInfos = null;
419 markNeedsLayout();
420 _removeSelectionRegistrarSubscription();
421 _disposeSelectableFragments();
422 _updateSelectionRegistrarSubscription();
423 }
424 }
425
426 /// The ongoing selections in this paragraph.
427 ///
428 /// The selection does not include selections in [PlaceholderSpan] if there
429 /// are any.
430 @visibleForTesting
431 List<TextSelection> get selections {
432 if (_lastSelectableFragments == null) {
433 return const <TextSelection>[];
434 }
435 final List<TextSelection> results = <TextSelection>[];
436 for (final _SelectableFragment fragment in _lastSelectableFragments!) {
437 if (fragment._textSelectionStart != null && fragment._textSelectionEnd != null) {
438 results.add(
439 TextSelection(
440 baseOffset: fragment._textSelectionStart!.offset,
441 extentOffset: fragment._textSelectionEnd!.offset,
442 ),
443 );
444 }
445 }
446 return results;
447 }
448
449 // Should be null if selection is not enabled, i.e. _registrar = null. The
450 // paragraph splits on [PlaceholderSpan.placeholderCodeUnit], and stores each
451 // fragment in this list.
452 List<_SelectableFragment>? _lastSelectableFragments;
453
454 /// The [SelectionRegistrar] this paragraph will be, or is, registered to.
455 SelectionRegistrar? get registrar => _registrar;
456 SelectionRegistrar? _registrar;
457 set registrar(SelectionRegistrar? value) {
458 if (value == _registrar) {
459 return;
460 }
461 _removeSelectionRegistrarSubscription();
462 _disposeSelectableFragments();
463 _registrar = value;
464 _updateSelectionRegistrarSubscription();
465 }
466
467 void _updateSelectionRegistrarSubscription() {
468 if (_registrar == null) {
469 return;
470 }
471 _lastSelectableFragments ??= _getSelectableFragments();
472 _lastSelectableFragments!.forEach(_registrar!.add);
473 if (_lastSelectableFragments!.isNotEmpty) {
474 markNeedsCompositingBitsUpdate();
475 }
476 }
477
478 void _removeSelectionRegistrarSubscription() {
479 if (_registrar == null || _lastSelectableFragments == null) {
480 return;
481 }
482 _lastSelectableFragments!.forEach(_registrar!.remove);
483 }
484
485 List<_SelectableFragment> _getSelectableFragments() {
486 final String plainText = text.toPlainText(includeSemanticsLabels: false);
487 final List<_SelectableFragment> result = <_SelectableFragment>[];
488 int start = 0;
489 while (start < plainText.length) {
490 int end = plainText.indexOf(_placeholderCharacter, start);
491 if (start != end) {
492 if (end == -1) {
493 end = plainText.length;
494 }
495 result.add(
496 _SelectableFragment(
497 paragraph: this,
498 range: TextRange(start: start, end: end),
499 fullText: plainText,
500 ),
501 );
502 start = end;
503 }
504 start += 1;
505 }
506 return result;
507 }
508
509 /// Determines whether the given [Selectable] was created by this
510 /// [RenderParagraph].
511 ///
512 /// The [RenderParagraph] splits its text into multiple [Selectable]s,
513 /// delimited by [PlaceholderSpan]s or [WidgetSpan]s.
514 bool selectableBelongsToParagraph(Selectable selectable) {
515 if (_lastSelectableFragments == null) {
516 return false;
517 }
518 return _lastSelectableFragments!.contains(selectable);
519 }
520
521 void _disposeSelectableFragments() {
522 if (_lastSelectableFragments == null) {
523 return;
524 }
525 for (final _SelectableFragment fragment in _lastSelectableFragments!) {
526 fragment.dispose();
527 }
528 _lastSelectableFragments = null;
529 }
530
531 @override
532 bool get alwaysNeedsCompositing => _lastSelectableFragments?.isNotEmpty ?? false;
533
534 @override
535 void markNeedsLayout() {
536 _lastSelectableFragments?.forEach(
537 (_SelectableFragment element) => element.didChangeParagraphLayout(),
538 );
539 super.markNeedsLayout();
540 }
541
542 @override
543 void dispose() {
544 _removeSelectionRegistrarSubscription();
545 _disposeSelectableFragments();
546 _textPainter.dispose();
547 _textIntrinsicsCache?.dispose();
548 super.dispose();
549 }
550
551 /// How the text should be aligned horizontally.
552 TextAlign get textAlign => _textPainter.textAlign;
553 set textAlign(TextAlign value) {
554 if (_textPainter.textAlign == value) {
555 return;
556 }
557 _textPainter.textAlign = value;
558 markNeedsPaint();
559 }
560
561 /// The directionality of the text.
562 ///
563 /// This decides how the [TextAlign.start], [TextAlign.end], and
564 /// [TextAlign.justify] values of [textAlign] are interpreted.
565 ///
566 /// This is also used to disambiguate how to render bidirectional text. For
567 /// example, if the [text] is an English phrase followed by a Hebrew phrase,
568 /// in a [TextDirection.ltr] context the English phrase will be on the left
569 /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
570 /// context, the English phrase will be on the right and the Hebrew phrase on
571 /// its left.
572 TextDirection get textDirection => _textPainter.textDirection!;
573 set textDirection(TextDirection value) {
574 if (_textPainter.textDirection == value) {
575 return;
576 }
577 _textPainter.textDirection = value;
578 markNeedsLayout();
579 }
580
581 /// Whether the text should break at soft line breaks.
582 ///
583 /// If false, the glyphs in the text will be positioned as if there was
584 /// unlimited horizontal space.
585 ///
586 /// If [softWrap] is false, [overflow] and [textAlign] may have unexpected
587 /// effects.
588 bool get softWrap => _softWrap;
589 bool _softWrap;
590 set softWrap(bool value) {
591 if (_softWrap == value) {
592 return;
593 }
594 _softWrap = value;
595 markNeedsLayout();
596 }
597
598 /// How visual overflow should be handled.
599 TextOverflow get overflow => _overflow;
600 TextOverflow _overflow;
601 set overflow(TextOverflow value) {
602 if (_overflow == value) {
603 return;
604 }
605 _overflow = value;
606 _textPainter.ellipsis = value == TextOverflow.ellipsis ? _kEllipsis : null;
607 markNeedsLayout();
608 }
609
610 /// Deprecated. Will be removed in a future version of Flutter. Use
611 /// [textScaler] instead.
612 ///
613 /// The number of font pixels for each logical pixel.
614 ///
615 /// For example, if the text scale factor is 1.5, text will be 50% larger than
616 /// the specified font size.
617 @Deprecated(
618 'Use textScaler instead. '
619 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
620 'This feature was deprecated after v3.12.0-2.0.pre.',
621 )
622 double get textScaleFactor => _textPainter.textScaleFactor;
623 @Deprecated(
624 'Use textScaler instead. '
625 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
626 'This feature was deprecated after v3.12.0-2.0.pre.',
627 )
628 set textScaleFactor(double value) {
629 textScaler = TextScaler.linear(value);
630 }
631
632 /// {@macro flutter.painting.textPainter.textScaler}
633 TextScaler get textScaler => _textPainter.textScaler;
634 set textScaler(TextScaler value) {
635 if (_textPainter.textScaler == value) {
636 return;
637 }
638 _textPainter.textScaler = value;
639 _overflowShader = null;
640 markNeedsLayout();
641 }
642
643 /// An optional maximum number of lines for the text to span, wrapping if
644 /// necessary. If the text exceeds the given number of lines, it will be
645 /// truncated according to [overflow] and [softWrap].
646 int? get maxLines => _textPainter.maxLines;
647
648 /// The value may be null. If it is not null, then it must be greater than
649 /// zero.
650 set maxLines(int? value) {
651 assert(value == null || value > 0);
652 if (_textPainter.maxLines == value) {
653 return;
654 }
655 _textPainter.maxLines = value;
656 _overflowShader = null;
657 markNeedsLayout();
658 }
659
660 /// Used by this paragraph's internal [TextPainter] to select a
661 /// locale-specific font.
662 ///
663 /// In some cases, the same Unicode character may be rendered differently
664 /// depending on the locale. For example, the '骨' character is rendered
665 /// differently in the Chinese and Japanese locales. In these cases, the
666 /// [locale] may be used to select a locale-specific font.
667 Locale? get locale => _textPainter.locale;
668
669 /// The value may be null.
670 set locale(Locale? value) {
671 if (_textPainter.locale == value) {
672 return;
673 }
674 _textPainter.locale = value;
675 _overflowShader = null;
676 markNeedsLayout();
677 }
678
679 /// {@macro flutter.painting.textPainter.strutStyle}
680 StrutStyle? get strutStyle => _textPainter.strutStyle;
681
682 /// The value may be null.
683 set strutStyle(StrutStyle? value) {
684 if (_textPainter.strutStyle == value) {
685 return;
686 }
687 _textPainter.strutStyle = value;
688 _overflowShader = null;
689 markNeedsLayout();
690 }
691
692 /// {@macro flutter.painting.textPainter.textWidthBasis}
693 TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis;
694 set textWidthBasis(TextWidthBasis value) {
695 if (_textPainter.textWidthBasis == value) {
696 return;
697 }
698 _textPainter.textWidthBasis = value;
699 _overflowShader = null;
700 markNeedsLayout();
701 }
702
703 /// {@macro dart.ui.textHeightBehavior}
704 ui.TextHeightBehavior? get textHeightBehavior => _textPainter.textHeightBehavior;
705 set textHeightBehavior(ui.TextHeightBehavior? value) {
706 if (_textPainter.textHeightBehavior == value) {
707 return;
708 }
709 _textPainter.textHeightBehavior = value;
710 _overflowShader = null;
711 markNeedsLayout();
712 }
713
714 /// The color to use when painting the selection.
715 ///
716 /// Ignored if the text is not selectable (e.g. if [registrar] is null).
717 Color? get selectionColor => _selectionColor;
718 Color? _selectionColor;
719 set selectionColor(Color? value) {
720 if (_selectionColor == value) {
721 return;
722 }
723 _selectionColor = value;
724 if (_lastSelectableFragments?.any(
725 (_SelectableFragment fragment) => fragment.value.hasSelection,
726 ) ??
727 false) {
728 markNeedsPaint();
729 }
730 }
731
732 Offset _getOffsetForPosition(TextPosition position) {
733 return getOffsetForCaret(position, Rect.zero) + Offset(0, getFullHeightForCaret(position));
734 }
735
736 @override
737 double computeMinIntrinsicWidth(double height) {
738 final List<PlaceholderDimensions> placeholderDimensions = layoutInlineChildren(
739 double.infinity,
740 (RenderBox child, BoxConstraints constraints) =>
741 Size(child.getMinIntrinsicWidth(double.infinity), 0.0),
742 ChildLayoutHelper.getDryBaseline,
743 );
744 return (_textIntrinsics
745 ..setPlaceholderDimensions(placeholderDimensions)
746 ..layout())
747 .minIntrinsicWidth;
748 }
749
750 @override
751 double computeMaxIntrinsicWidth(double height) {
752 final List<PlaceholderDimensions> placeholderDimensions = layoutInlineChildren(
753 double.infinity,
754 // Height and baseline is irrelevant as all text will be laid
755 // out in a single line. Therefore, using 0.0 as a dummy for the height.
756 (RenderBox child, BoxConstraints constraints) =>
757 Size(child.getMaxIntrinsicWidth(double.infinity), 0.0),
758 ChildLayoutHelper.getDryBaseline,
759 );
760 return (_textIntrinsics
761 ..setPlaceholderDimensions(placeholderDimensions)
762 ..layout())
763 .maxIntrinsicWidth;
764 }
765
766 /// An estimate of the height of a line in the text. See [TextPainter.preferredLineHeight].
767 ///
768 /// This does not require the layout to be updated.
769 @visibleForTesting
770 double get preferredLineHeight => _textPainter.preferredLineHeight;
771
772 double _computeIntrinsicHeight(double width) {
773 return (_textIntrinsics
774 ..setPlaceholderDimensions(
775 layoutInlineChildren(
776 width,
777 ChildLayoutHelper.dryLayoutChild,
778 ChildLayoutHelper.getDryBaseline,
779 ),
780 )
781 ..layout(minWidth: width, maxWidth: _adjustMaxWidth(width)))
782 .height;
783 }
784
785 @override
786 double computeMinIntrinsicHeight(double width) {
787 return _computeIntrinsicHeight(width);
788 }
789
790 @override
791 double computeMaxIntrinsicHeight(double width) {
792 return _computeIntrinsicHeight(width);
793 }
794
795 @override
796 bool hitTestSelf(Offset position) => true;
797
798 @override
799 @protected
800 bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
801 final GlyphInfo? glyph = _textPainter.getClosestGlyphForOffset(position);
802 // The hit-test can't fall through the horizontal gaps between visually
803 // adjacent characters on the same line, even with a large letter-spacing or
804 // text justification, as graphemeClusterLayoutBounds.width is the advance
805 // width to the next character, so there's no gap between their
806 // graphemeClusterLayoutBounds rects.
807 final InlineSpan? spanHit =
808 glyph != null && glyph.graphemeClusterLayoutBounds.contains(position)
809 ? _textPainter.text!.getSpanForPosition(
810 TextPosition(offset: glyph.graphemeClusterCodeUnitRange.start),
811 )
812 : null;
813 switch (spanHit) {
814 case final HitTestTarget span:
815 result.add(HitTestEntry(span));
816 return true;
817 case _:
818 return hitTestInlineChildren(result, position);
819 }
820 }
821
822 bool _needsClipping = false;
823 ui.Shader? _overflowShader;
824
825 /// Whether this paragraph currently has a [dart:ui.Shader] for its overflow
826 /// effect.
827 ///
828 /// Used to test this object. Not for use in production.
829 @visibleForTesting
830 bool get debugHasOverflowShader => _overflowShader != null;
831
832 @override
833 void systemFontsDidChange() {
834 super.systemFontsDidChange();
835 _textPainter.markNeedsLayout();
836 }
837
838 // Placeholder dimensions representing the sizes of child inline widgets.
839 //
840 // These need to be cached because the text painter's placeholder dimensions
841 // will be overwritten during intrinsic width/height calculations and must be
842 // restored to the original values before final layout and painting.
843 List<PlaceholderDimensions>? _placeholderDimensions;
844
845 double _adjustMaxWidth(double maxWidth) {
846 return softWrap || overflow == TextOverflow.ellipsis ? maxWidth : double.infinity;
847 }
848
849 void _layoutTextWithConstraints(BoxConstraints constraints) {
850 _textPainter
851 ..setPlaceholderDimensions(_placeholderDimensions)
852 ..layout(minWidth: constraints.minWidth, maxWidth: _adjustMaxWidth(constraints.maxWidth));
853 }
854
855 @override
856 @protected
857 Size computeDryLayout(covariant BoxConstraints constraints) {
858 final Size size =
859 (_textIntrinsics
860 ..setPlaceholderDimensions(
861 layoutInlineChildren(
862 constraints.maxWidth,
863 ChildLayoutHelper.dryLayoutChild,
864 ChildLayoutHelper.getDryBaseline,
865 ),
866 )
867 ..layout(
868 minWidth: constraints.minWidth,
869 maxWidth: _adjustMaxWidth(constraints.maxWidth),
870 ))
871 .size;
872 return constraints.constrain(size);
873 }
874
875 @override
876 double computeDistanceToActualBaseline(TextBaseline baseline) {
877 assert(!debugNeedsLayout);
878 assert(constraints.debugAssertIsValid());
879 _layoutTextWithConstraints(constraints);
880 // TODO(garyq): Since our metric for ideographic baseline is currently
881 // inaccurate and the non-alphabetic baselines are based off of the
882 // alphabetic baseline, we use the alphabetic for now to produce correct
883 // layouts. We should eventually change this back to pass the `baseline`
884 // property when the ideographic baseline is properly implemented
885 // (https://github.com/flutter/flutter/issues/22625).
886 return _textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic);
887 }
888
889 @override
890 double computeDryBaseline(covariant BoxConstraints constraints, TextBaseline baseline) {
891 assert(constraints.debugAssertIsValid());
892 _textIntrinsics
893 ..setPlaceholderDimensions(
894 layoutInlineChildren(
895 constraints.maxWidth,
896 ChildLayoutHelper.dryLayoutChild,
897 ChildLayoutHelper.getDryBaseline,
898 ),
899 )
900 ..layout(minWidth: constraints.minWidth, maxWidth: _adjustMaxWidth(constraints.maxWidth));
901 return _textIntrinsics.computeDistanceToActualBaseline(TextBaseline.alphabetic);
902 }
903
904 @override
905 void performLayout() {
906 _lastSelectableFragments?.forEach(
907 (_SelectableFragment element) => element.didChangeParagraphLayout(),
908 );
909 final BoxConstraints constraints = this.constraints;
910 _placeholderDimensions = layoutInlineChildren(
911 constraints.maxWidth,
912 ChildLayoutHelper.layoutChild,
913 ChildLayoutHelper.getBaseline,
914 );
915 _layoutTextWithConstraints(constraints);
916 positionInlineChildren(_textPainter.inlinePlaceholderBoxes!);
917
918 final Size textSize = _textPainter.size;
919 size = constraints.constrain(textSize);
920
921 final bool didOverflowHeight = size.height < textSize.height || _textPainter.didExceedMaxLines;
922 final bool didOverflowWidth = size.width < textSize.width;
923 // TODO(abarth): We're only measuring the sizes of the line boxes here. If
924 // the glyphs draw outside the line boxes, we might think that there isn't
925 // visual overflow when there actually is visual overflow. This can become
926 // a problem if we start having horizontal overflow and introduce a clip
927 // that affects the actual (but undetected) vertical overflow.
928 final bool hasVisualOverflow = didOverflowWidth || didOverflowHeight;
929 if (hasVisualOverflow) {
930 switch (_overflow) {
931 case TextOverflow.visible:
932 _needsClipping = false;
933 _overflowShader = null;
934 case TextOverflow.clip:
935 case TextOverflow.ellipsis:
936 _needsClipping = true;
937 _overflowShader = null;
938 case TextOverflow.fade:
939 _needsClipping = true;
940 final TextPainter fadeSizePainter = TextPainter(
941 text: TextSpan(style: _textPainter.text!.style, text: '\u2026'),
942 textDirection: textDirection,
943 textScaler: textScaler,
944 locale: locale,
945 )..layout();
946 if (didOverflowWidth) {
947 final (double fadeStart, double fadeEnd) = switch (textDirection) {
948 TextDirection.rtl => (fadeSizePainter.width, 0.0),
949 TextDirection.ltr => (size.width - fadeSizePainter.width, size.width),
950 };
951 _overflowShader = ui.Gradient.linear(
952 Offset(fadeStart, 0.0),
953 Offset(fadeEnd, 0.0),
954 <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
955 );
956 } else {
957 final double fadeEnd = size.height;
958 final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0;
959 _overflowShader = ui.Gradient.linear(
960 Offset(0.0, fadeStart),
961 Offset(0.0, fadeEnd),
962 <Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
963 );
964 }
965 fadeSizePainter.dispose();
966 }
967 } else {
968 _needsClipping = false;
969 _overflowShader = null;
970 }
971 }
972
973 @override
974 void applyPaintTransform(RenderBox child, Matrix4 transform) {
975 defaultApplyPaintTransform(child, transform);
976 }
977
978 @override
979 void paint(PaintingContext context, Offset offset) {
980 // Text alignment only triggers repaint so it's possible the text layout has
981 // been invalidated but performLayout wasn't called at this point. Make sure
982 // the TextPainter has a valid layout.
983 _layoutTextWithConstraints(constraints);
984 assert(() {
985 if (debugRepaintTextRainbowEnabled) {
986 final Paint paint = Paint()..color = debugCurrentRepaintColor.toColor();
987 context.canvas.drawRect(offset & size, paint);
988 }
989 return true;
990 }());
991
992 if (_needsClipping) {
993 final Rect bounds = offset & size;
994 if (_overflowShader != null) {
995 // This layer limits what the shader below blends with to be just the
996 // text (as opposed to the text and its background).
997 context.canvas.saveLayer(bounds, Paint());
998 } else {
999 context.canvas.save();
1000 }
1001 context.canvas.clipRect(bounds);
1002 }
1003
1004 if (_lastSelectableFragments != null) {
1005 for (final _SelectableFragment fragment in _lastSelectableFragments!) {
1006 fragment.paint(context, offset);
1007 }
1008 }
1009
1010 _textPainter.paint(context.canvas, offset);
1011
1012 paintInlineChildren(context, offset);
1013
1014 if (_needsClipping) {
1015 if (_overflowShader != null) {
1016 context.canvas.translate(offset.dx, offset.dy);
1017 final Paint paint = Paint()
1018 ..blendMode = BlendMode.modulate
1019 ..shader = _overflowShader;
1020 context.canvas.drawRect(Offset.zero & size, paint);
1021 }
1022 context.canvas.restore();
1023 }
1024 }
1025
1026 /// Returns the offset at which to paint the caret.
1027 ///
1028 /// Valid only after [layout].
1029 Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
1030 assert(!debugNeedsLayout);
1031 _layoutTextWithConstraints(constraints);
1032 return _textPainter.getOffsetForCaret(position, caretPrototype);
1033 }
1034
1035 /// {@macro flutter.painting.textPainter.getFullHeightForCaret}
1036 ///
1037 /// Valid only after [layout].
1038 double getFullHeightForCaret(TextPosition position) {
1039 assert(!debugNeedsLayout);
1040 _layoutTextWithConstraints(constraints);
1041 return _textPainter.getFullHeightForCaret(position, Rect.zero);
1042 }
1043
1044 /// Returns a list of rects that bound the given selection.
1045 ///
1046 /// The [boxHeightStyle] and [boxWidthStyle] arguments may be used to select
1047 /// the shape of the [TextBox]es. These properties default to
1048 /// [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively.
1049 ///
1050 /// A given selection might have more than one rect if the [RenderParagraph]
1051 /// contains multiple [InlineSpan]s or bidirectional text, because logically
1052 /// contiguous text might not be visually contiguous.
1053 ///
1054 /// Valid only after [layout].
1055 ///
1056 /// See also:
1057 ///
1058 /// * [TextPainter.getBoxesForSelection], the method in TextPainter to get
1059 /// the equivalent boxes.
1060 List<ui.TextBox> getBoxesForSelection(
1061 TextSelection selection, {
1062 ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
1063 ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
1064 }) {
1065 assert(!debugNeedsLayout);
1066 _layoutTextWithConstraints(constraints);
1067 return _textPainter.getBoxesForSelection(
1068 selection,
1069 boxHeightStyle: boxHeightStyle,
1070 boxWidthStyle: boxWidthStyle,
1071 );
1072 }
1073
1074 /// Returns the position within the text for the given pixel offset.
1075 ///
1076 /// Valid only after [layout].
1077 TextPosition getPositionForOffset(Offset offset) {
1078 assert(!debugNeedsLayout);
1079 _layoutTextWithConstraints(constraints);
1080 return _textPainter.getPositionForOffset(offset);
1081 }
1082
1083 /// Returns the text range of the word at the given offset. Characters not
1084 /// part of a word, such as spaces, symbols, and punctuation, have word breaks
1085 /// on both sides. In such cases, this method will return a text range that
1086 /// contains the given text position.
1087 ///
1088 /// Word boundaries are defined more precisely in Unicode Standard Annex #29
1089 /// <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
1090 ///
1091 /// Valid only after [layout].
1092 TextRange getWordBoundary(TextPosition position) {
1093 assert(!debugNeedsLayout);
1094 _layoutTextWithConstraints(constraints);
1095 return _textPainter.getWordBoundary(position);
1096 }
1097
1098 TextRange _getLineAtOffset(TextPosition position) => _textPainter.getLineBoundary(position);
1099
1100 TextPosition _getTextPositionAbove(TextPosition position) {
1101 // -0.5 of preferredLineHeight points to the middle of the line above.
1102 final double preferredLineHeight = _textPainter.preferredLineHeight;
1103 final double verticalOffset = -0.5 * preferredLineHeight;
1104 return _getTextPositionVertical(position, verticalOffset);
1105 }
1106
1107 TextPosition _getTextPositionBelow(TextPosition position) {
1108 // 1.5 of preferredLineHeight points to the middle of the line below.
1109 final double preferredLineHeight = _textPainter.preferredLineHeight;
1110 final double verticalOffset = 1.5 * preferredLineHeight;
1111 return _getTextPositionVertical(position, verticalOffset);
1112 }
1113
1114 TextPosition _getTextPositionVertical(TextPosition position, double verticalOffset) {
1115 final Offset caretOffset = _textPainter.getOffsetForCaret(position, Rect.zero);
1116 final Offset caretOffsetTranslated = caretOffset.translate(0.0, verticalOffset);
1117 return _textPainter.getPositionForOffset(caretOffsetTranslated);
1118 }
1119
1120 /// Returns the size of the text as laid out.
1121 ///
1122 /// This can differ from [size] if the text overflowed or if the [constraints]
1123 /// provided by the parent [RenderObject] forced the layout to be bigger than
1124 /// necessary for the given [text].
1125 ///
1126 /// This returns the [TextPainter.size] of the underlying [TextPainter].
1127 ///
1128 /// Valid only after [layout].
1129 Size get textSize {
1130 assert(!debugNeedsLayout);
1131 return _textPainter.size;
1132 }
1133
1134 /// Whether the text was truncated or ellipsized as laid out.
1135 ///
1136 /// This returns the [TextPainter.didExceedMaxLines] of the underlying [TextPainter].
1137 ///
1138 /// Valid only after [layout].
1139 bool get didExceedMaxLines {
1140 assert(!debugNeedsLayout);
1141 return _textPainter.didExceedMaxLines;
1142 }
1143
1144 /// Collected during [describeSemanticsConfiguration], used by
1145 /// [assembleSemanticsNode].
1146 List<InlineSpanSemanticsInformation>? _semanticsInfo;
1147
1148 @override
1149 void describeSemanticsConfiguration(SemanticsConfiguration config) {
1150 super.describeSemanticsConfiguration(config);
1151 _semanticsInfo = text.getSemanticsInformation();
1152 bool needsAssembleSemanticsNode = false;
1153 bool needsChildConfigurationsDelegate = false;
1154 for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
1155 if (info.recognizer != null || info.semanticsIdentifier != null) {
1156 needsAssembleSemanticsNode = true;
1157 break;
1158 }
1159 needsChildConfigurationsDelegate = needsChildConfigurationsDelegate || info.isPlaceholder;
1160 }
1161
1162 if (needsAssembleSemanticsNode) {
1163 config.explicitChildNodes = true;
1164 config.isSemanticBoundary = true;
1165 } else if (needsChildConfigurationsDelegate) {
1166 config.childConfigurationsDelegate = _childSemanticsConfigurationsDelegate;
1167 } else {
1168 if (_cachedAttributedLabels == null) {
1169 final StringBuffer buffer = StringBuffer();
1170 int offset = 0;
1171 final List<StringAttribute> attributes = <StringAttribute>[];
1172 for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
1173 final String label = info.semanticsLabel ?? info.text;
1174 for (final StringAttribute infoAttribute in info.stringAttributes) {
1175 final TextRange originalRange = infoAttribute.range;
1176 attributes.add(
1177 infoAttribute.copy(
1178 range: TextRange(
1179 start: offset + originalRange.start,
1180 end: offset + originalRange.end,
1181 ),
1182 ),
1183 );
1184 }
1185 buffer.write(label);
1186 offset += label.length;
1187 }
1188 _cachedAttributedLabels = <AttributedString>[
1189 AttributedString(buffer.toString(), attributes: attributes),
1190 ];
1191 }
1192 config.attributedLabel = _cachedAttributedLabels![0];
1193 config.textDirection = textDirection;
1194 }
1195 }
1196
1197 ChildSemanticsConfigurationsResult _childSemanticsConfigurationsDelegate(
1198 List<SemanticsConfiguration> childConfigs,
1199 ) {
1200 final ChildSemanticsConfigurationsResultBuilder builder =
1201 ChildSemanticsConfigurationsResultBuilder();
1202 int placeholderIndex = 0;
1203 int childConfigsIndex = 0;
1204 int attributedLabelCacheIndex = 0;
1205 InlineSpanSemanticsInformation? seenTextInfo;
1206 _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
1207 for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) {
1208 if (info.isPlaceholder) {
1209 if (seenTextInfo != null) {
1210 builder.markAsMergeUp(
1211 _createSemanticsConfigForTextInfo(seenTextInfo, attributedLabelCacheIndex),
1212 );
1213 attributedLabelCacheIndex += 1;
1214 }
1215 // Mark every childConfig belongs to this placeholder to merge up group.
1216 while (childConfigsIndex < childConfigs.length &&
1217 childConfigs[childConfigsIndex].tagsChildrenWith(
1218 PlaceholderSpanIndexSemanticsTag(placeholderIndex),
1219 )) {
1220 builder.markAsMergeUp(childConfigs[childConfigsIndex]);
1221 childConfigsIndex += 1;
1222 }
1223 placeholderIndex += 1;
1224 } else {
1225 seenTextInfo = info;
1226 }
1227 }
1228
1229 // Handle plain text info at the end.
1230 if (seenTextInfo != null) {
1231 builder.markAsMergeUp(
1232 _createSemanticsConfigForTextInfo(seenTextInfo, attributedLabelCacheIndex),
1233 );
1234 }
1235 return builder.build();
1236 }
1237
1238 SemanticsConfiguration _createSemanticsConfigForTextInfo(
1239 InlineSpanSemanticsInformation textInfo,
1240 int cacheIndex,
1241 ) {
1242 assert(!textInfo.requiresOwnNode);
1243 final List<AttributedString> cachedStrings = _cachedAttributedLabels ??= <AttributedString>[];
1244 assert(cacheIndex <= cachedStrings.length);
1245 final bool hasCache = cacheIndex < cachedStrings.length;
1246
1247 late AttributedString attributedLabel;
1248 if (hasCache) {
1249 attributedLabel = cachedStrings[cacheIndex];
1250 } else {
1251 assert(cachedStrings.length == cacheIndex);
1252 attributedLabel = AttributedString(
1253 textInfo.semanticsLabel ?? textInfo.text,
1254 attributes: textInfo.stringAttributes,
1255 );
1256 cachedStrings.add(attributedLabel);
1257 }
1258 return SemanticsConfiguration()
1259 ..textDirection = textDirection
1260 ..attributedLabel = attributedLabel;
1261 }
1262
1263 // Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
1264 // can be re-used when [assembleSemanticsNode] is called again. This ensures
1265 // stable ids for the [SemanticsNode]s of [TextSpan]s across
1266 // [assembleSemanticsNode] invocations.
1267 Map<Key, SemanticsNode>? _cachedChildNodes;
1268
1269 @override
1270 void assembleSemanticsNode(
1271 SemanticsNode node,
1272 SemanticsConfiguration config,
1273 Iterable<SemanticsNode> children,
1274 ) {
1275 assert(_semanticsInfo != null && _semanticsInfo!.isNotEmpty);
1276 final List<SemanticsNode> newChildren = <SemanticsNode>[];
1277 TextDirection currentDirection = textDirection;
1278 Rect currentRect;
1279 double ordinal = 0.0;
1280 int start = 0;
1281 int placeholderIndex = 0;
1282 int childIndex = 0;
1283 RenderBox? child = firstChild;
1284 final Map<Key, SemanticsNode> newChildCache = <Key, SemanticsNode>{};
1285 _cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
1286 for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) {
1287 final TextSelection selection = TextSelection(
1288 baseOffset: start,
1289 extentOffset: start + info.text.length,
1290 );
1291 start += info.text.length;
1292
1293 if (info.isPlaceholder) {
1294 // A placeholder span may have 0 to multiple semantics nodes, we need
1295 // to annotate all of the semantics nodes belong to this span.
1296 while (children.length > childIndex &&
1297 children
1298 .elementAt(childIndex)
1299 .isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
1300 final SemanticsNode childNode = children.elementAt(childIndex);
1301 final TextParentData parentData = child!.parentData! as TextParentData;
1302 // parentData.scale may be null if the render object is truncated.
1303 if (parentData.offset != null) {
1304 newChildren.add(childNode);
1305 }
1306 childIndex += 1;
1307 }
1308 child = childAfter(child!);
1309 placeholderIndex += 1;
1310 } else {
1311 final TextDirection initialDirection = currentDirection;
1312 final List<ui.TextBox> rects = getBoxesForSelection(selection);
1313 if (rects.isEmpty) {
1314 continue;
1315 }
1316 Rect rect = rects.first.toRect();
1317 currentDirection = rects.first.direction;
1318 for (final ui.TextBox textBox in rects.skip(1)) {
1319 rect = rect.expandToInclude(textBox.toRect());
1320 currentDirection = textBox.direction;
1321 }
1322 // Any of the text boxes may have had infinite dimensions.
1323 // We shouldn't pass infinite dimensions up to the bridges.
1324 rect = Rect.fromLTWH(
1325 math.max(0.0, rect.left),
1326 math.max(0.0, rect.top),
1327 math.min(rect.width, constraints.maxWidth),
1328 math.min(rect.height, constraints.maxHeight),
1329 );
1330 // round the current rectangle to make this API testable and add some
1331 // padding so that the accessibility rects do not overlap with the text.
1332 currentRect = Rect.fromLTRB(
1333 rect.left.floorToDouble() - 4.0,
1334 rect.top.floorToDouble() - 4.0,
1335 rect.right.ceilToDouble() + 4.0,
1336 rect.bottom.ceilToDouble() + 4.0,
1337 );
1338 final SemanticsConfiguration configuration = SemanticsConfiguration()
1339 ..sortKey = OrdinalSortKey(ordinal++)
1340 ..textDirection = initialDirection
1341 ..identifier = info.semanticsIdentifier ?? ''
1342 ..attributedLabel = AttributedString(
1343 info.semanticsLabel ?? info.text,
1344 attributes: info.stringAttributes,
1345 );
1346 switch (info.recognizer) {
1347 case TapGestureRecognizer(onTap: final VoidCallback? handler):
1348 case DoubleTapGestureRecognizer(onDoubleTap: final VoidCallback? handler):
1349 if (handler != null) {
1350 configuration.onTap = handler;
1351 configuration.isLink = true;
1352 }
1353 case LongPressGestureRecognizer(onLongPress: final GestureLongPressCallback? onLongPress):
1354 if (onLongPress != null) {
1355 configuration.onLongPress = onLongPress;
1356 }
1357 case null:
1358 break;
1359 default:
1360 assert(false, '${info.recognizer.runtimeType} is not supported.');
1361 }
1362 if (node.parentPaintClipRect != null) {
1363 final Rect paintRect = node.parentPaintClipRect!.intersect(currentRect);
1364 configuration.isHidden = paintRect.isEmpty && !currentRect.isEmpty;
1365 }
1366 final SemanticsNode newChild;
1367 if (_cachedChildNodes?.isNotEmpty ?? false) {
1368 newChild = _cachedChildNodes!.remove(_cachedChildNodes!.keys.first)!;
1369 } else {
1370 final UniqueKey key = UniqueKey();
1371 newChild = SemanticsNode(key: key, showOnScreen: _createShowOnScreenFor(key));
1372 }
1373 newChild
1374 ..updateWith(config: configuration)
1375 ..rect = currentRect;
1376 newChildCache[newChild.key!] = newChild;
1377 newChildren.add(newChild);
1378 }
1379 }
1380 // Makes sure we annotated all of the semantics children.
1381 assert(childIndex == children.length);
1382 assert(child == null);
1383
1384 _cachedChildNodes = newChildCache;
1385 node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
1386 }
1387
1388 VoidCallback? _createShowOnScreenFor(Key key) {
1389 return () {
1390 final SemanticsNode node = _cachedChildNodes![key]!;
1391 showOnScreen(descendant: this, rect: node.rect);
1392 };
1393 }
1394
1395 @override
1396 void clearSemantics() {
1397 super.clearSemantics();
1398 _cachedChildNodes = null;
1399 }
1400
1401 @override
1402 List<DiagnosticsNode> debugDescribeChildren() {
1403 return <DiagnosticsNode>[
1404 text.toDiagnosticsNode(name: 'text', style: DiagnosticsTreeStyle.transition),
1405 ];
1406 }
1407
1408 @override
1409 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1410 super.debugFillProperties(properties);
1411 properties.add(EnumProperty<TextAlign>('textAlign', textAlign));
1412 properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
1413 properties.add(
1414 FlagProperty(
1415 'softWrap',
1416 value: softWrap,
1417 ifTrue: 'wrapping at box width',
1418 ifFalse: 'no wrapping except at line break characters',
1419 showName: true,
1420 ),
1421 );
1422 properties.add(EnumProperty<TextOverflow>('overflow', overflow));
1423 properties.add(
1424 DiagnosticsProperty<TextScaler>('textScaler', textScaler, defaultValue: TextScaler.noScaling),
1425 );
1426 properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
1427 properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
1428 }
1429}
1430
1431/// A continuous, selectable piece of paragraph.
1432///
1433/// Since the selections in [PlaceholderSpan] are handled independently in its
1434/// subtree, a selection in [RenderParagraph] can't continue across a
1435/// [PlaceholderSpan]. The [RenderParagraph] splits itself on [PlaceholderSpan]
1436/// to create multiple `_SelectableFragment`s so that they can be selected
1437/// separately.
1438class _SelectableFragment
1439 with Selectable, Diagnosticable, ChangeNotifier
1440 implements TextLayoutMetrics {
1441 _SelectableFragment({required this.paragraph, required this.fullText, required this.range})
1442 : assert(range.isValid && !range.isCollapsed && range.isNormalized) {
1443 if (kFlutterMemoryAllocationsEnabled) {
1444 ChangeNotifier.maybeDispatchObjectCreation(this);
1445 }
1446 _selectionGeometry = _getSelectionGeometry();
1447 }
1448
1449 final TextRange range;
1450 final RenderParagraph paragraph;
1451 final String fullText;
1452
1453 TextPosition? _textSelectionStart;
1454 TextPosition? _textSelectionEnd;
1455
1456 bool _selectableContainsOriginTextBoundary = false;
1457
1458 LayerLink? _startHandleLayerLink;
1459 LayerLink? _endHandleLayerLink;
1460
1461 @override
1462 SelectionGeometry get value => _selectionGeometry;
1463 late SelectionGeometry _selectionGeometry;
1464 void _updateSelectionGeometry() {
1465 final SelectionGeometry newValue = _getSelectionGeometry();
1466
1467 if (_selectionGeometry == newValue) {
1468 return;
1469 }
1470 _selectionGeometry = newValue;
1471 notifyListeners();
1472 }
1473
1474 SelectionGeometry _getSelectionGeometry() {
1475 if (_textSelectionStart == null || _textSelectionEnd == null) {
1476 return const SelectionGeometry(status: SelectionStatus.none, hasContent: true);
1477 }
1478
1479 final int selectionStart = _textSelectionStart!.offset;
1480 final int selectionEnd = _textSelectionEnd!.offset;
1481 final bool isReversed = selectionStart > selectionEnd;
1482 final Offset startOffsetInParagraphCoordinates = paragraph._getOffsetForPosition(
1483 TextPosition(offset: selectionStart),
1484 );
1485 final Offset endOffsetInParagraphCoordinates = selectionStart == selectionEnd
1486 ? startOffsetInParagraphCoordinates
1487 : paragraph._getOffsetForPosition(TextPosition(offset: selectionEnd));
1488 final bool flipHandles = isReversed != (TextDirection.rtl == paragraph.textDirection);
1489 final TextSelection selection = TextSelection(
1490 baseOffset: selectionStart,
1491 extentOffset: selectionEnd,
1492 );
1493 final List<Rect> selectionRects = <Rect>[];
1494 for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) {
1495 selectionRects.add(textBox.toRect());
1496 }
1497 final bool selectionCollapsed = selectionStart == selectionEnd;
1498 final (
1499 TextSelectionHandleType startSelectionHandleType,
1500 TextSelectionHandleType endSelectionHandleType,
1501 ) = switch ((selectionCollapsed, flipHandles)) {
1502 // Always prefer collapsed handle when selection is collapsed.
1503 (true, _) => (TextSelectionHandleType.collapsed, TextSelectionHandleType.collapsed),
1504 (false, true) => (TextSelectionHandleType.right, TextSelectionHandleType.left),
1505 (false, false) => (TextSelectionHandleType.left, TextSelectionHandleType.right),
1506 };
1507 return SelectionGeometry(
1508 startSelectionPoint: SelectionPoint(
1509 localPosition: startOffsetInParagraphCoordinates,
1510 lineHeight: paragraph._textPainter.preferredLineHeight,
1511 handleType: startSelectionHandleType,
1512 ),
1513 endSelectionPoint: SelectionPoint(
1514 localPosition: endOffsetInParagraphCoordinates,
1515 lineHeight: paragraph._textPainter.preferredLineHeight,
1516 handleType: endSelectionHandleType,
1517 ),
1518 selectionRects: selectionRects,
1519 status: selectionCollapsed ? SelectionStatus.collapsed : SelectionStatus.uncollapsed,
1520 hasContent: true,
1521 );
1522 }
1523
1524 @override
1525 SelectionResult dispatchSelectionEvent(SelectionEvent event) {
1526 late final SelectionResult result;
1527 final TextPosition? existingSelectionStart = _textSelectionStart;
1528 final TextPosition? existingSelectionEnd = _textSelectionEnd;
1529 switch (event.type) {
1530 case SelectionEventType.startEdgeUpdate:
1531 case SelectionEventType.endEdgeUpdate:
1532 final SelectionEdgeUpdateEvent edgeUpdate = event as SelectionEdgeUpdateEvent;
1533 final TextGranularity granularity = event.granularity;
1534
1535 switch (granularity) {
1536 case TextGranularity.character:
1537 result = _updateSelectionEdge(
1538 edgeUpdate.globalPosition,
1539 isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate,
1540 );
1541 case TextGranularity.word:
1542 result = _updateSelectionEdgeByTextBoundary(
1543 edgeUpdate.globalPosition,
1544 isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate,
1545 getTextBoundary: _getWordBoundaryAtPosition,
1546 );
1547 case TextGranularity.paragraph:
1548 result = _updateSelectionEdgeByMultiSelectableTextBoundary(
1549 edgeUpdate.globalPosition,
1550 isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate,
1551 getTextBoundary: _getParagraphBoundaryAtPosition,
1552 getClampedTextBoundary: _getClampedParagraphBoundaryAtPosition,
1553 );
1554 case TextGranularity.document:
1555 case TextGranularity.line:
1556 assert(false, 'Moving the selection edge by line or document is not supported.');
1557 }
1558 case SelectionEventType.clear:
1559 result = _handleClearSelection();
1560 case SelectionEventType.selectAll:
1561 result = _handleSelectAll();
1562 case SelectionEventType.selectWord:
1563 final SelectWordSelectionEvent selectWord = event as SelectWordSelectionEvent;
1564 result = _handleSelectWord(selectWord.globalPosition);
1565 case SelectionEventType.selectParagraph:
1566 final SelectParagraphSelectionEvent selectParagraph =
1567 event as SelectParagraphSelectionEvent;
1568 if (selectParagraph.absorb) {
1569 _handleSelectAll();
1570 result = SelectionResult.next;
1571 _selectableContainsOriginTextBoundary = true;
1572 } else {
1573 result = _handleSelectParagraph(selectParagraph.globalPosition);
1574 }
1575 case SelectionEventType.granularlyExtendSelection:
1576 final GranularlyExtendSelectionEvent granularlyExtendSelection =
1577 event as GranularlyExtendSelectionEvent;
1578 result = _handleGranularlyExtendSelection(
1579 granularlyExtendSelection.forward,
1580 granularlyExtendSelection.isEnd,
1581 granularlyExtendSelection.granularity,
1582 );
1583 case SelectionEventType.directionallyExtendSelection:
1584 final DirectionallyExtendSelectionEvent directionallyExtendSelection =
1585 event as DirectionallyExtendSelectionEvent;
1586 result = _handleDirectionallyExtendSelection(
1587 directionallyExtendSelection.dx,
1588 directionallyExtendSelection.isEnd,
1589 directionallyExtendSelection.direction,
1590 );
1591 }
1592
1593 if (existingSelectionStart != _textSelectionStart ||
1594 existingSelectionEnd != _textSelectionEnd) {
1595 _didChangeSelection();
1596 }
1597 return result;
1598 }
1599
1600 @override
1601 SelectedContent? getSelectedContent() {
1602 if (_textSelectionStart == null || _textSelectionEnd == null) {
1603 return null;
1604 }
1605 final int start = math.min(_textSelectionStart!.offset, _textSelectionEnd!.offset);
1606 final int end = math.max(_textSelectionStart!.offset, _textSelectionEnd!.offset);
1607 return SelectedContent(plainText: fullText.substring(start, end));
1608 }
1609
1610 @override
1611 SelectedContentRange? getSelection() {
1612 if (_textSelectionStart == null || _textSelectionEnd == null) {
1613 return null;
1614 }
1615 return SelectedContentRange(
1616 startOffset: _textSelectionStart!.offset,
1617 endOffset: _textSelectionEnd!.offset,
1618 );
1619 }
1620
1621 void _didChangeSelection() {
1622 paragraph.markNeedsPaint();
1623 _updateSelectionGeometry();
1624 }
1625
1626 TextPosition _updateSelectionStartEdgeByTextBoundary(
1627 _TextBoundaryRecord? textBoundary,
1628 _TextBoundaryAtPosition getTextBoundary,
1629 TextPosition position,
1630 TextPosition? existingSelectionStart,
1631 TextPosition? existingSelectionEnd,
1632 ) {
1633 TextPosition? targetPosition;
1634 if (textBoundary != null) {
1635 assert(
1636 textBoundary.boundaryStart.offset >= range.start &&
1637 textBoundary.boundaryEnd.offset <= range.end,
1638 );
1639 if (_selectableContainsOriginTextBoundary &&
1640 existingSelectionStart != null &&
1641 existingSelectionEnd != null) {
1642 final bool isSamePosition = position.offset == existingSelectionEnd.offset;
1643 final bool isSelectionInverted =
1644 existingSelectionStart.offset > existingSelectionEnd.offset;
1645 final bool shouldSwapEdges =
1646 !isSamePosition &&
1647 (isSelectionInverted != (position.offset > existingSelectionEnd.offset));
1648 if (shouldSwapEdges) {
1649 if (position.offset < existingSelectionEnd.offset) {
1650 targetPosition = textBoundary.boundaryStart;
1651 } else {
1652 targetPosition = textBoundary.boundaryEnd;
1653 }
1654 // When the selection is inverted by the new position it is necessary to
1655 // swap the start edge (moving edge) with the end edge (static edge) to
1656 // maintain the origin text boundary within the selection.
1657 final _TextBoundaryRecord localTextBoundary = getTextBoundary(existingSelectionEnd);
1658 assert(
1659 localTextBoundary.boundaryStart.offset >= range.start &&
1660 localTextBoundary.boundaryEnd.offset <= range.end,
1661 );
1662 _setSelectionPosition(
1663 existingSelectionEnd.offset == localTextBoundary.boundaryStart.offset
1664 ? localTextBoundary.boundaryEnd
1665 : localTextBoundary.boundaryStart,
1666 isEnd: true,
1667 );
1668 } else {
1669 if (position.offset < existingSelectionEnd.offset) {
1670 targetPosition = textBoundary.boundaryStart;
1671 } else if (position.offset > existingSelectionEnd.offset) {
1672 targetPosition = textBoundary.boundaryEnd;
1673 } else {
1674 // Keep the origin text boundary in bounds when position is at the static edge.
1675 targetPosition = existingSelectionStart;
1676 }
1677 }
1678 } else {
1679 if (existingSelectionEnd != null) {
1680 // If the end edge exists and the start edge is being moved, then the
1681 // start edge is moved to encompass the entire text boundary at the new position.
1682 if (position.offset < existingSelectionEnd.offset) {
1683 targetPosition = textBoundary.boundaryStart;
1684 } else {
1685 targetPosition = textBoundary.boundaryEnd;
1686 }
1687 } else {
1688 // Move the start edge to the closest text boundary.
1689 targetPosition = _closestTextBoundary(textBoundary, position);
1690 }
1691 }
1692 } else {
1693 // The position is not contained within the current rect. The targetPosition
1694 // will either be at the end or beginning of the current rect. See [SelectionUtils.adjustDragOffset]
1695 // for a more in depth explanation on this adjustment.
1696 if (_selectableContainsOriginTextBoundary &&
1697 existingSelectionStart != null &&
1698 existingSelectionEnd != null) {
1699 // When the selection is inverted by the new position it is necessary to
1700 // swap the start edge (moving edge) with the end edge (static edge) to
1701 // maintain the origin text boundary within the selection.
1702 final bool isSamePosition = position.offset == existingSelectionEnd.offset;
1703 final bool isSelectionInverted =
1704 existingSelectionStart.offset > existingSelectionEnd.offset;
1705 final bool shouldSwapEdges =
1706 !isSamePosition &&
1707 (isSelectionInverted != (position.offset > existingSelectionEnd.offset));
1708
1709 if (shouldSwapEdges) {
1710 final _TextBoundaryRecord localTextBoundary = getTextBoundary(existingSelectionEnd);
1711 assert(
1712 localTextBoundary.boundaryStart.offset >= range.start &&
1713 localTextBoundary.boundaryEnd.offset <= range.end,
1714 );
1715 _setSelectionPosition(
1716 isSelectionInverted ? localTextBoundary.boundaryEnd : localTextBoundary.boundaryStart,
1717 isEnd: true,
1718 );
1719 }
1720 }
1721 }
1722 return targetPosition ?? position;
1723 }
1724
1725 TextPosition _updateSelectionEndEdgeByTextBoundary(
1726 _TextBoundaryRecord? textBoundary,
1727 _TextBoundaryAtPosition getTextBoundary,
1728 TextPosition position,
1729 TextPosition? existingSelectionStart,
1730 TextPosition? existingSelectionEnd,
1731 ) {
1732 TextPosition? targetPosition;
1733 if (textBoundary != null) {
1734 assert(
1735 textBoundary.boundaryStart.offset >= range.start &&
1736 textBoundary.boundaryEnd.offset <= range.end,
1737 );
1738 if (_selectableContainsOriginTextBoundary &&
1739 existingSelectionStart != null &&
1740 existingSelectionEnd != null) {
1741 final bool isSamePosition = position.offset == existingSelectionStart.offset;
1742 final bool isSelectionInverted =
1743 existingSelectionStart.offset > existingSelectionEnd.offset;
1744 final bool shouldSwapEdges =
1745 !isSamePosition &&
1746 (isSelectionInverted != (position.offset < existingSelectionStart.offset));
1747 if (shouldSwapEdges) {
1748 if (position.offset < existingSelectionStart.offset) {
1749 targetPosition = textBoundary.boundaryStart;
1750 } else {
1751 targetPosition = textBoundary.boundaryEnd;
1752 }
1753 // When the selection is inverted by the new position it is necessary to
1754 // swap the end edge (moving edge) with the start edge (static edge) to
1755 // maintain the origin text boundary within the selection.
1756 final _TextBoundaryRecord localTextBoundary = getTextBoundary(existingSelectionStart);
1757 assert(
1758 localTextBoundary.boundaryStart.offset >= range.start &&
1759 localTextBoundary.boundaryEnd.offset <= range.end,
1760 );
1761 _setSelectionPosition(
1762 existingSelectionStart.offset == localTextBoundary.boundaryStart.offset
1763 ? localTextBoundary.boundaryEnd
1764 : localTextBoundary.boundaryStart,
1765 isEnd: false,
1766 );
1767 } else {
1768 if (position.offset < existingSelectionStart.offset) {
1769 targetPosition = textBoundary.boundaryStart;
1770 } else if (position.offset > existingSelectionStart.offset) {
1771 targetPosition = textBoundary.boundaryEnd;
1772 } else {
1773 // Keep the origin text boundary in bounds when position is at the static edge.
1774 targetPosition = existingSelectionEnd;
1775 }
1776 }
1777 } else {
1778 if (existingSelectionStart != null) {
1779 // If the start edge exists and the end edge is being moved, then the
1780 // end edge is moved to encompass the entire text boundary at the new position.
1781 if (position.offset < existingSelectionStart.offset) {
1782 targetPosition = textBoundary.boundaryStart;
1783 } else {
1784 targetPosition = textBoundary.boundaryEnd;
1785 }
1786 } else {
1787 // Move the end edge to the closest text boundary.
1788 targetPosition = _closestTextBoundary(textBoundary, position);
1789 }
1790 }
1791 } else {
1792 // The position is not contained within the current rect. The targetPosition
1793 // will either be at the end or beginning of the current rect. See [SelectionUtils.adjustDragOffset]
1794 // for a more in depth explanation on this adjustment.
1795 if (_selectableContainsOriginTextBoundary &&
1796 existingSelectionStart != null &&
1797 existingSelectionEnd != null) {
1798 // When the selection is inverted by the new position it is necessary to
1799 // swap the end edge (moving edge) with the start edge (static edge) to
1800 // maintain the origin text boundary within the selection.
1801 final bool isSamePosition = position.offset == existingSelectionStart.offset;
1802 final bool isSelectionInverted =
1803 existingSelectionStart.offset > existingSelectionEnd.offset;
1804 final bool shouldSwapEdges =
1805 isSelectionInverted != (position.offset < existingSelectionStart.offset) ||
1806 isSamePosition;
1807 if (shouldSwapEdges) {
1808 final _TextBoundaryRecord localTextBoundary = getTextBoundary(existingSelectionStart);
1809 assert(
1810 localTextBoundary.boundaryStart.offset >= range.start &&
1811 localTextBoundary.boundaryEnd.offset <= range.end,
1812 );
1813 _setSelectionPosition(
1814 isSelectionInverted ? localTextBoundary.boundaryStart : localTextBoundary.boundaryEnd,
1815 isEnd: false,
1816 );
1817 }
1818 }
1819 }
1820 return targetPosition ?? position;
1821 }
1822
1823 SelectionResult _updateSelectionEdgeByTextBoundary(
1824 Offset globalPosition, {
1825 required bool isEnd,
1826 required _TextBoundaryAtPosition getTextBoundary,
1827 }) {
1828 // When the start/end edges are swapped, i.e. the start is after the end, and
1829 // the scrollable synthesizes an event for the opposite edge, this will potentially
1830 // move the opposite edge outside of the origin text boundary and we are unable to recover.
1831 final TextPosition? existingSelectionStart = _textSelectionStart;
1832 final TextPosition? existingSelectionEnd = _textSelectionEnd;
1833
1834 _setSelectionPosition(null, isEnd: isEnd);
1835 final Matrix4 transform = paragraph.getTransformTo(null);
1836 transform.invert();
1837 final Offset localPosition = MatrixUtils.transformPoint(transform, globalPosition);
1838 if (_rect.isEmpty) {
1839 return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
1840 }
1841 final Offset adjustedOffset = SelectionUtils.adjustDragOffset(
1842 _rect,
1843 localPosition,
1844 direction: paragraph.textDirection,
1845 );
1846
1847 final TextPosition position = paragraph.getPositionForOffset(adjustedOffset);
1848 // Check if the original local position is within the rect, if it is not then
1849 // we do not need to look up the text boundary for that position. This is to
1850 // maintain a selectables selection collapsed at 0 when the local position is
1851 // not located inside its rect.
1852 _TextBoundaryRecord? textBoundary = _rect.contains(localPosition)
1853 ? getTextBoundary(position)
1854 : null;
1855 if (textBoundary != null &&
1856 (textBoundary.boundaryStart.offset < range.start &&
1857 textBoundary.boundaryEnd.offset <= range.start ||
1858 textBoundary.boundaryStart.offset >= range.end &&
1859 textBoundary.boundaryEnd.offset > range.end)) {
1860 // When the position is located at a placeholder inside of the text, then we may compute
1861 // a text boundary that does not belong to the current selectable fragment. In this case
1862 // we should invalidate the text boundary so that it is not taken into account when
1863 // computing the target position.
1864 textBoundary = null;
1865 }
1866 final TextPosition targetPosition = _clampTextPosition(
1867 isEnd
1868 ? _updateSelectionEndEdgeByTextBoundary(
1869 textBoundary,
1870 getTextBoundary,
1871 position,
1872 existingSelectionStart,
1873 existingSelectionEnd,
1874 )
1875 : _updateSelectionStartEdgeByTextBoundary(
1876 textBoundary,
1877 getTextBoundary,
1878 position,
1879 existingSelectionStart,
1880 existingSelectionEnd,
1881 ),
1882 );
1883
1884 _setSelectionPosition(targetPosition, isEnd: isEnd);
1885 if (targetPosition.offset == range.end) {
1886 return SelectionResult.next;
1887 }
1888
1889 if (targetPosition.offset == range.start) {
1890 return SelectionResult.previous;
1891 }
1892 // TODO(chunhtai): The geometry information should not be used to determine
1893 // selection result. This is a workaround to RenderParagraph, where it does
1894 // not have a way to get accurate text length if its text is truncated due to
1895 // layout constraint.
1896 return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
1897 }
1898
1899 SelectionResult _updateSelectionEdge(Offset globalPosition, {required bool isEnd}) {
1900 _setSelectionPosition(null, isEnd: isEnd);
1901 final Matrix4 transform = paragraph.getTransformTo(null);
1902 transform.invert();
1903 final Offset localPosition = MatrixUtils.transformPoint(transform, globalPosition);
1904 if (_rect.isEmpty) {
1905 return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
1906 }
1907 final Offset adjustedOffset = SelectionUtils.adjustDragOffset(
1908 _rect,
1909 localPosition,
1910 direction: paragraph.textDirection,
1911 );
1912
1913 final TextPosition position = _clampTextPosition(
1914 paragraph.getPositionForOffset(adjustedOffset),
1915 );
1916 _setSelectionPosition(position, isEnd: isEnd);
1917 if (position.offset == range.end) {
1918 return SelectionResult.next;
1919 }
1920 if (position.offset == range.start) {
1921 return SelectionResult.previous;
1922 }
1923 // TODO(chunhtai): The geometry information should not be used to determine
1924 // selection result. This is a workaround to RenderParagraph, where it does
1925 // not have a way to get accurate text length if its text is truncated due to
1926 // layout constraint.
1927 return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
1928 }
1929
1930 // This method handles updating the start edge by a text boundary that may
1931 // not be contained within this selectable fragment. It is possible
1932 // that a boundary spans multiple selectable fragments when the text contains
1933 // [WidgetSpan]s.
1934 //
1935 // This method differs from [_updateSelectionStartEdgeByTextBoundary] in that
1936 // to pivot offset used to swap selection edges and maintain the origin
1937 // text boundary selected may be located outside of this selectable fragment.
1938 //
1939 // See [_updateSelectionEndEdgeByMultiSelectableTextBoundary] for the method
1940 // that handles updating the end edge.
1941 SelectionResult? _updateSelectionStartEdgeByMultiSelectableTextBoundary(
1942 _TextBoundaryAtPositionInText getTextBoundary,
1943 bool paragraphContainsPosition,
1944 TextPosition position,
1945 TextPosition? existingSelectionStart,
1946 TextPosition? existingSelectionEnd,
1947 ) {
1948 const bool isEnd = false;
1949 if (_selectableContainsOriginTextBoundary &&
1950 existingSelectionStart != null &&
1951 existingSelectionEnd != null) {
1952 // If this selectable contains the origin boundary, maintain the existing
1953 // selection.
1954 final bool forwardSelection = existingSelectionEnd.offset >= existingSelectionStart.offset;
1955 if (paragraphContainsPosition) {
1956 // When the position is within the root paragraph, swap the start and end
1957 // edges when the selection is inverted.
1958 final _TextBoundaryRecord boundaryAtPosition = getTextBoundary(position, fullText);
1959 // To accurately retrieve the origin text boundary when the selection
1960 // is forward, use existingSelectionEnd.offset - 1. This is necessary
1961 // because in a forwards selection, existingSelectionEnd marks the end
1962 // of the origin text boundary. Using the unmodified offset incorrectly
1963 // targets the subsequent text boundary.
1964 final _TextBoundaryRecord originTextBoundary = getTextBoundary(
1965 forwardSelection
1966 ? TextPosition(
1967 offset: existingSelectionEnd.offset - 1,
1968 affinity: existingSelectionEnd.affinity,
1969 )
1970 : existingSelectionEnd,
1971 fullText,
1972 );
1973 final TextPosition targetPosition;
1974 final int pivotOffset = forwardSelection
1975 ? originTextBoundary.boundaryEnd.offset
1976 : originTextBoundary.boundaryStart.offset;
1977 final bool shouldSwapEdges = !forwardSelection != (position.offset > pivotOffset);
1978 if (position.offset < pivotOffset) {
1979 targetPosition = boundaryAtPosition.boundaryStart;
1980 } else if (position.offset > pivotOffset) {
1981 targetPosition = boundaryAtPosition.boundaryEnd;
1982 } else {
1983 // Keep the origin text boundary in bounds when position is at the static edge.
1984 targetPosition = forwardSelection ? existingSelectionStart : existingSelectionEnd;
1985 }
1986 if (shouldSwapEdges) {
1987 _setSelectionPosition(
1988 _clampTextPosition(
1989 forwardSelection ? originTextBoundary.boundaryStart : originTextBoundary.boundaryEnd,
1990 ),
1991 isEnd: true,
1992 );
1993 }
1994 _setSelectionPosition(_clampTextPosition(targetPosition), isEnd: isEnd);
1995 final bool finalSelectionIsForward =
1996 _textSelectionEnd!.offset >= _textSelectionStart!.offset;
1997 if (boundaryAtPosition.boundaryStart.offset > range.end &&
1998 boundaryAtPosition.boundaryEnd.offset > range.end) {
1999 return SelectionResult.next;
2000 }
2001 if (boundaryAtPosition.boundaryStart.offset < range.start &&
2002 boundaryAtPosition.boundaryEnd.offset < range.start) {
2003 return SelectionResult.previous;
2004 }
2005 if (finalSelectionIsForward) {
2006 if (boundaryAtPosition.boundaryStart.offset >= originTextBoundary.boundaryStart.offset) {
2007 return SelectionResult.end;
2008 }
2009 if (boundaryAtPosition.boundaryStart.offset < originTextBoundary.boundaryStart.offset) {
2010 return SelectionResult.previous;
2011 }
2012 } else {
2013 if (boundaryAtPosition.boundaryEnd.offset <= originTextBoundary.boundaryEnd.offset) {
2014 return SelectionResult.end;
2015 }
2016 if (boundaryAtPosition.boundaryEnd.offset > originTextBoundary.boundaryEnd.offset) {
2017 return SelectionResult.next;
2018 }
2019 }
2020 } else {
2021 // When the drag position is not contained within the root paragraph,
2022 // swap the edges when the selection changes direction.
2023 final TextPosition clampedPosition = _clampTextPosition(position);
2024 // To accurately retrieve the origin text boundary when the selection
2025 // is forward, use existingSelectionEnd.offset - 1. This is necessary
2026 // because in a forwards selection, existingSelectionEnd marks the end
2027 // of the origin text boundary. Using the unmodified offset incorrectly
2028 // targets the subsequent text boundary.
2029 final _TextBoundaryRecord originTextBoundary = getTextBoundary(
2030 forwardSelection
2031 ? TextPosition(
2032 offset: existingSelectionEnd.offset - 1,
2033 affinity: existingSelectionEnd.affinity,
2034 )
2035 : existingSelectionEnd,
2036 fullText,
2037 );
2038 if (forwardSelection && clampedPosition.offset == range.start) {
2039 _setSelectionPosition(clampedPosition, isEnd: isEnd);
2040 return SelectionResult.previous;
2041 }
2042 if (!forwardSelection && clampedPosition.offset == range.end) {
2043 _setSelectionPosition(clampedPosition, isEnd: isEnd);
2044 return SelectionResult.next;
2045 }
2046 if (forwardSelection && clampedPosition.offset == range.end) {
2047 _setSelectionPosition(_clampTextPosition(originTextBoundary.boundaryStart), isEnd: true);
2048 _setSelectionPosition(clampedPosition, isEnd: isEnd);
2049 return SelectionResult.next;
2050 }
2051 if (!forwardSelection && clampedPosition.offset == range.start) {
2052 _setSelectionPosition(_clampTextPosition(originTextBoundary.boundaryEnd), isEnd: true);
2053 _setSelectionPosition(clampedPosition, isEnd: isEnd);
2054 return SelectionResult.previous;
2055 }
2056 }
2057 } else {
2058 // A paragraph boundary may not be completely contained within this root
2059 // selectable fragment. Keep searching until we find the end of the
2060 // boundary. Do not search when the current drag position is on a placeholder
2061 // to allow traversal to reach that placeholder.
2062 final bool positionOnPlaceholder =
2063 paragraph.getWordBoundary(position).textInside(fullText) == _placeholderCharacter;
2064 if (!paragraphContainsPosition || positionOnPlaceholder) {
2065 return null;
2066 }
2067 if (existingSelectionEnd != null) {
2068 final _TextBoundaryRecord boundaryAtPosition = getTextBoundary(position, fullText);
2069 final bool backwardSelection =
2070 existingSelectionStart == null && existingSelectionEnd.offset == range.start ||
2071 existingSelectionStart == existingSelectionEnd &&
2072 existingSelectionEnd.offset == range.start ||
2073 existingSelectionStart != null &&
2074 existingSelectionStart.offset > existingSelectionEnd.offset;
2075 if (boundaryAtPosition.boundaryStart.offset < range.start &&
2076 boundaryAtPosition.boundaryEnd.offset < range.start) {
2077 _setSelectionPosition(TextPosition(offset: range.start), isEnd: isEnd);
2078 return SelectionResult.previous;
2079 }
2080 if (boundaryAtPosition.boundaryStart.offset > range.end &&
2081 boundaryAtPosition.boundaryEnd.offset > range.end) {
2082 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2083 return SelectionResult.next;
2084 }
2085 if (backwardSelection) {
2086 if (boundaryAtPosition.boundaryEnd.offset <= range.end) {
2087 _setSelectionPosition(_clampTextPosition(boundaryAtPosition.boundaryEnd), isEnd: isEnd);
2088 return SelectionResult.end;
2089 }
2090 if (boundaryAtPosition.boundaryEnd.offset > range.end) {
2091 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2092 return SelectionResult.next;
2093 }
2094 } else {
2095 _setSelectionPosition(_clampTextPosition(boundaryAtPosition.boundaryStart), isEnd: isEnd);
2096 if (boundaryAtPosition.boundaryStart.offset < range.start) {
2097 return SelectionResult.previous;
2098 }
2099 if (boundaryAtPosition.boundaryStart.offset >= range.start) {
2100 return SelectionResult.end;
2101 }
2102 }
2103 }
2104 }
2105 return null;
2106 }
2107
2108 // This method handles updating the end edge by a text boundary that may
2109 // not be contained within this selectable fragment. It is possible
2110 // that a boundary spans multiple selectable fragments when the text contains
2111 // [WidgetSpan]s.
2112 //
2113 // This method differs from [_updateSelectionEndEdgeByTextBoundary] in that
2114 // to pivot offset used to swap selection edges and maintain the origin
2115 // text boundary selected may be located outside of this selectable fragment.
2116 //
2117 // See [_updateSelectionStartEdgeByMultiSelectableTextBoundary] for the method
2118 // that handles updating the end edge.
2119 SelectionResult? _updateSelectionEndEdgeByMultiSelectableTextBoundary(
2120 _TextBoundaryAtPositionInText getTextBoundary,
2121 bool paragraphContainsPosition,
2122 TextPosition position,
2123 TextPosition? existingSelectionStart,
2124 TextPosition? existingSelectionEnd,
2125 ) {
2126 const bool isEnd = true;
2127 if (_selectableContainsOriginTextBoundary &&
2128 existingSelectionStart != null &&
2129 existingSelectionEnd != null) {
2130 // If this selectable contains the origin boundary, maintain the existing
2131 // selection.
2132 final bool forwardSelection = existingSelectionEnd.offset >= existingSelectionStart.offset;
2133 if (paragraphContainsPosition) {
2134 // When the position is within the root paragraph, swap the start and end
2135 // edges when the selection is inverted.
2136 final _TextBoundaryRecord boundaryAtPosition = getTextBoundary(position, fullText);
2137 // To accurately retrieve the origin text boundary when the selection
2138 // is backwards, use existingSelectionStart.offset - 1. This is necessary
2139 // because in a backwards selection, existingSelectionStart marks the end
2140 // of the origin text boundary. Using the unmodified offset incorrectly
2141 // targets the subsequent text boundary.
2142 final _TextBoundaryRecord originTextBoundary = getTextBoundary(
2143 forwardSelection
2144 ? existingSelectionStart
2145 : TextPosition(
2146 offset: existingSelectionStart.offset - 1,
2147 affinity: existingSelectionStart.affinity,
2148 ),
2149 fullText,
2150 );
2151 final TextPosition targetPosition;
2152 final int pivotOffset = forwardSelection
2153 ? originTextBoundary.boundaryStart.offset
2154 : originTextBoundary.boundaryEnd.offset;
2155 final bool shouldSwapEdges = !forwardSelection != (position.offset < pivotOffset);
2156 if (position.offset < pivotOffset) {
2157 targetPosition = boundaryAtPosition.boundaryStart;
2158 } else if (position.offset > pivotOffset) {
2159 targetPosition = boundaryAtPosition.boundaryEnd;
2160 } else {
2161 // Keep the origin text boundary in bounds when position is at the static edge.
2162 targetPosition = forwardSelection ? existingSelectionEnd : existingSelectionStart;
2163 }
2164 if (shouldSwapEdges) {
2165 _setSelectionPosition(
2166 _clampTextPosition(
2167 forwardSelection ? originTextBoundary.boundaryEnd : originTextBoundary.boundaryStart,
2168 ),
2169 isEnd: false,
2170 );
2171 }
2172 _setSelectionPosition(_clampTextPosition(targetPosition), isEnd: isEnd);
2173 final bool finalSelectionIsForward =
2174 _textSelectionEnd!.offset >= _textSelectionStart!.offset;
2175 if (boundaryAtPosition.boundaryStart.offset > range.end &&
2176 boundaryAtPosition.boundaryEnd.offset > range.end) {
2177 return SelectionResult.next;
2178 }
2179 if (boundaryAtPosition.boundaryStart.offset < range.start &&
2180 boundaryAtPosition.boundaryEnd.offset < range.start) {
2181 return SelectionResult.previous;
2182 }
2183 if (finalSelectionIsForward) {
2184 if (boundaryAtPosition.boundaryEnd.offset <= originTextBoundary.boundaryEnd.offset) {
2185 return SelectionResult.end;
2186 }
2187 if (boundaryAtPosition.boundaryEnd.offset > originTextBoundary.boundaryEnd.offset) {
2188 return SelectionResult.next;
2189 }
2190 } else {
2191 if (boundaryAtPosition.boundaryStart.offset >= originTextBoundary.boundaryStart.offset) {
2192 return SelectionResult.end;
2193 }
2194 if (boundaryAtPosition.boundaryStart.offset < originTextBoundary.boundaryStart.offset) {
2195 return SelectionResult.previous;
2196 }
2197 }
2198 } else {
2199 // When the drag position is not contained within the root paragraph,
2200 // swap the edges when the selection changes direction.
2201 final TextPosition clampedPosition = _clampTextPosition(position);
2202 // To accurately retrieve the origin text boundary when the selection
2203 // is backwards, use existingSelectionStart.offset - 1. This is necessary
2204 // because in a backwards selection, existingSelectionStart marks the end
2205 // of the origin text boundary. Using the unmodified offset incorrectly
2206 // targets the subsequent text boundary.
2207 final _TextBoundaryRecord originTextBoundary = getTextBoundary(
2208 forwardSelection
2209 ? existingSelectionStart
2210 : TextPosition(
2211 offset: existingSelectionStart.offset - 1,
2212 affinity: existingSelectionStart.affinity,
2213 ),
2214 fullText,
2215 );
2216 if (forwardSelection && clampedPosition.offset == range.start) {
2217 _setSelectionPosition(_clampTextPosition(originTextBoundary.boundaryEnd), isEnd: false);
2218 _setSelectionPosition(clampedPosition, isEnd: isEnd);
2219 return SelectionResult.previous;
2220 }
2221 if (!forwardSelection && clampedPosition.offset == range.end) {
2222 _setSelectionPosition(_clampTextPosition(originTextBoundary.boundaryStart), isEnd: false);
2223 _setSelectionPosition(clampedPosition, isEnd: isEnd);
2224 return SelectionResult.next;
2225 }
2226 if (forwardSelection && clampedPosition.offset == range.end) {
2227 _setSelectionPosition(clampedPosition, isEnd: isEnd);
2228 return SelectionResult.next;
2229 }
2230 if (!forwardSelection && clampedPosition.offset == range.start) {
2231 _setSelectionPosition(clampedPosition, isEnd: isEnd);
2232 return SelectionResult.previous;
2233 }
2234 }
2235 } else {
2236 // A paragraph boundary may not be completely contained within this root
2237 // selectable fragment. Keep searching until we find the end of the
2238 // boundary. Do not search when the current drag position is on a placeholder
2239 // to allow traversal to reach that placeholder.
2240 final bool positionOnPlaceholder =
2241 paragraph.getWordBoundary(position).textInside(fullText) == _placeholderCharacter;
2242 if (!paragraphContainsPosition || positionOnPlaceholder) {
2243 return null;
2244 }
2245 if (existingSelectionStart != null) {
2246 final _TextBoundaryRecord boundaryAtPosition = getTextBoundary(position, fullText);
2247 final bool backwardSelection =
2248 existingSelectionEnd == null && existingSelectionStart.offset == range.end ||
2249 existingSelectionStart == existingSelectionEnd &&
2250 existingSelectionStart.offset == range.end ||
2251 existingSelectionEnd != null &&
2252 existingSelectionStart.offset > existingSelectionEnd.offset;
2253 if (boundaryAtPosition.boundaryStart.offset < range.start &&
2254 boundaryAtPosition.boundaryEnd.offset < range.start) {
2255 _setSelectionPosition(TextPosition(offset: range.start), isEnd: isEnd);
2256 return SelectionResult.previous;
2257 }
2258 if (boundaryAtPosition.boundaryStart.offset > range.end &&
2259 boundaryAtPosition.boundaryEnd.offset > range.end) {
2260 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2261 return SelectionResult.next;
2262 }
2263 if (backwardSelection) {
2264 _setSelectionPosition(_clampTextPosition(boundaryAtPosition.boundaryStart), isEnd: isEnd);
2265 if (boundaryAtPosition.boundaryStart.offset < range.start) {
2266 return SelectionResult.previous;
2267 }
2268 if (boundaryAtPosition.boundaryStart.offset >= range.start) {
2269 return SelectionResult.end;
2270 }
2271 } else {
2272 if (boundaryAtPosition.boundaryEnd.offset <= range.end) {
2273 _setSelectionPosition(_clampTextPosition(boundaryAtPosition.boundaryEnd), isEnd: isEnd);
2274 return SelectionResult.end;
2275 }
2276 if (boundaryAtPosition.boundaryEnd.offset > range.end) {
2277 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2278 return SelectionResult.next;
2279 }
2280 }
2281 }
2282 }
2283 return null;
2284 }
2285
2286 // The placeholder character used by [RenderParagraph].
2287 static final String _placeholderCharacter = String.fromCharCode(
2288 PlaceholderSpan.placeholderCodeUnit,
2289 );
2290 static final int _placeholderLength = _placeholderCharacter.length;
2291 // This method handles updating the start edge by a text boundary that may
2292 // not be contained within this selectable fragment. It is possible
2293 // that a boundary spans multiple selectable fragments when the text contains
2294 // [WidgetSpan]s.
2295 //
2296 // This method differs from [_updateSelectionStartEdgeByMultiSelectableBoundary]
2297 // in that to maintain the origin text boundary selected at a placeholder,
2298 // this selectable fragment must be aware of the [RenderParagraph] that closely
2299 // encompasses the complete origin text boundary.
2300 //
2301 // See [_updateSelectionEndEdgeAtPlaceholderByMultiSelectableTextBoundary] for the method
2302 // that handles updating the end edge.
2303 SelectionResult? _updateSelectionStartEdgeAtPlaceholderByMultiSelectableTextBoundary(
2304 _TextBoundaryAtPositionInText getTextBoundary,
2305 Offset globalPosition,
2306 bool paragraphContainsPosition,
2307 TextPosition position,
2308 TextPosition? existingSelectionStart,
2309 TextPosition? existingSelectionEnd,
2310 ) {
2311 const bool isEnd = false;
2312 if (_selectableContainsOriginTextBoundary &&
2313 existingSelectionStart != null &&
2314 existingSelectionEnd != null) {
2315 // If this selectable contains the origin boundary, maintain the existing
2316 // selection.
2317 final bool forwardSelection = existingSelectionEnd.offset >= existingSelectionStart.offset;
2318 final RenderParagraph originParagraph = _getOriginParagraph();
2319 final bool fragmentBelongsToOriginParagraph = originParagraph == paragraph;
2320 if (fragmentBelongsToOriginParagraph) {
2321 return _updateSelectionStartEdgeByMultiSelectableTextBoundary(
2322 getTextBoundary,
2323 paragraphContainsPosition,
2324 position,
2325 existingSelectionStart,
2326 existingSelectionEnd,
2327 );
2328 }
2329 final Matrix4 originTransform = originParagraph.getTransformTo(null);
2330 originTransform.invert();
2331 final Offset originParagraphLocalPosition = MatrixUtils.transformPoint(
2332 originTransform,
2333 globalPosition,
2334 );
2335 final bool positionWithinOriginParagraph = originParagraph.paintBounds.contains(
2336 originParagraphLocalPosition,
2337 );
2338 final TextPosition positionRelativeToOriginParagraph = originParagraph.getPositionForOffset(
2339 originParagraphLocalPosition,
2340 );
2341 if (positionWithinOriginParagraph) {
2342 // When the selection is inverted by the new position it is necessary to
2343 // swap the start edge (moving edge) with the end edge (static edge) to
2344 // maintain the origin text boundary within the selection.
2345 final String originText = originParagraph.text.toPlainText(includeSemanticsLabels: false);
2346 final _TextBoundaryRecord boundaryAtPosition = getTextBoundary(
2347 positionRelativeToOriginParagraph,
2348 originText,
2349 );
2350 final _TextBoundaryRecord originTextBoundary = getTextBoundary(
2351 _getPositionInParagraph(originParagraph),
2352 originText,
2353 );
2354 final TextPosition targetPosition;
2355 final int pivotOffset = forwardSelection
2356 ? originTextBoundary.boundaryEnd.offset
2357 : originTextBoundary.boundaryStart.offset;
2358 final bool shouldSwapEdges =
2359 !forwardSelection != (positionRelativeToOriginParagraph.offset > pivotOffset);
2360 if (positionRelativeToOriginParagraph.offset < pivotOffset) {
2361 targetPosition = boundaryAtPosition.boundaryStart;
2362 } else if (positionRelativeToOriginParagraph.offset > pivotOffset) {
2363 targetPosition = boundaryAtPosition.boundaryEnd;
2364 } else {
2365 // Keep the origin text boundary in bounds when position is at the static edge.
2366 targetPosition = existingSelectionStart;
2367 }
2368 if (shouldSwapEdges) {
2369 _setSelectionPosition(existingSelectionStart, isEnd: true);
2370 }
2371 _setSelectionPosition(_clampTextPosition(targetPosition), isEnd: isEnd);
2372 final bool finalSelectionIsForward =
2373 _textSelectionEnd!.offset >= _textSelectionStart!.offset;
2374 final TextPosition originParagraphPlaceholderTextPosition = _getPositionInParagraph(
2375 originParagraph,
2376 );
2377 final TextRange originParagraphPlaceholderRange = TextRange(
2378 start: originParagraphPlaceholderTextPosition.offset,
2379 end: originParagraphPlaceholderTextPosition.offset + _placeholderLength,
2380 );
2381 if (boundaryAtPosition.boundaryStart.offset > originParagraphPlaceholderRange.end &&
2382 boundaryAtPosition.boundaryEnd.offset > originParagraphPlaceholderRange.end) {
2383 return SelectionResult.next;
2384 }
2385 if (boundaryAtPosition.boundaryStart.offset < originParagraphPlaceholderRange.start &&
2386 boundaryAtPosition.boundaryEnd.offset < originParagraphPlaceholderRange.start) {
2387 return SelectionResult.previous;
2388 }
2389 if (finalSelectionIsForward) {
2390 if (boundaryAtPosition.boundaryEnd.offset <= originTextBoundary.boundaryEnd.offset) {
2391 return SelectionResult.end;
2392 }
2393 if (boundaryAtPosition.boundaryEnd.offset > originTextBoundary.boundaryEnd.offset) {
2394 return SelectionResult.next;
2395 }
2396 } else {
2397 if (boundaryAtPosition.boundaryStart.offset >= originTextBoundary.boundaryStart.offset) {
2398 return SelectionResult.end;
2399 }
2400 if (boundaryAtPosition.boundaryStart.offset < originTextBoundary.boundaryStart.offset) {
2401 return SelectionResult.previous;
2402 }
2403 }
2404 } else {
2405 // When the drag position is not contained within the origin paragraph,
2406 // swap the edges when the selection changes direction.
2407 //
2408 // [SelectionUtils.adjustDragOffset] will adjust the given [Offset] to the
2409 // beginning or end of the provided [Rect] based on whether the [Offset]
2410 // is located within the given [Rect].
2411 final Offset adjustedOffset = SelectionUtils.adjustDragOffset(
2412 originParagraph.paintBounds,
2413 originParagraphLocalPosition,
2414 direction: paragraph.textDirection,
2415 );
2416 final TextPosition adjustedPositionRelativeToOriginParagraph = originParagraph
2417 .getPositionForOffset(adjustedOffset);
2418 final TextPosition originParagraphPlaceholderTextPosition = _getPositionInParagraph(
2419 originParagraph,
2420 );
2421 final TextRange originParagraphPlaceholderRange = TextRange(
2422 start: originParagraphPlaceholderTextPosition.offset,
2423 end: originParagraphPlaceholderTextPosition.offset + _placeholderLength,
2424 );
2425 if (forwardSelection &&
2426 adjustedPositionRelativeToOriginParagraph.offset <=
2427 originParagraphPlaceholderRange.start) {
2428 _setSelectionPosition(TextPosition(offset: range.start), isEnd: isEnd);
2429 return SelectionResult.previous;
2430 }
2431 if (!forwardSelection &&
2432 adjustedPositionRelativeToOriginParagraph.offset >=
2433 originParagraphPlaceholderRange.end) {
2434 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2435 return SelectionResult.next;
2436 }
2437 if (forwardSelection &&
2438 adjustedPositionRelativeToOriginParagraph.offset >=
2439 originParagraphPlaceholderRange.end) {
2440 _setSelectionPosition(existingSelectionStart, isEnd: true);
2441 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2442 return SelectionResult.next;
2443 }
2444 if (!forwardSelection &&
2445 adjustedPositionRelativeToOriginParagraph.offset <=
2446 originParagraphPlaceholderRange.start) {
2447 _setSelectionPosition(existingSelectionStart, isEnd: true);
2448 _setSelectionPosition(TextPosition(offset: range.start), isEnd: isEnd);
2449 return SelectionResult.previous;
2450 }
2451 }
2452 } else {
2453 // When the drag position is somewhere on the root text and not a placeholder,
2454 // traverse the selectable fragments relative to the [RenderParagraph] that
2455 // contains the drag position.
2456 if (paragraphContainsPosition) {
2457 return _updateSelectionStartEdgeByMultiSelectableTextBoundary(
2458 getTextBoundary,
2459 paragraphContainsPosition,
2460 position,
2461 existingSelectionStart,
2462 existingSelectionEnd,
2463 );
2464 }
2465 if (existingSelectionEnd != null) {
2466 final ({RenderParagraph paragraph, Offset localPosition})? targetDetails =
2467 _getParagraphContainingPosition(globalPosition);
2468 if (targetDetails == null) {
2469 return null;
2470 }
2471 final RenderParagraph targetParagraph = targetDetails.paragraph;
2472 final TextPosition positionRelativeToTargetParagraph = targetParagraph.getPositionForOffset(
2473 targetDetails.localPosition,
2474 );
2475 final String targetText = targetParagraph.text.toPlainText(includeSemanticsLabels: false);
2476 final bool positionOnPlaceholder =
2477 targetParagraph
2478 .getWordBoundary(positionRelativeToTargetParagraph)
2479 .textInside(targetText) ==
2480 _placeholderCharacter;
2481 if (positionOnPlaceholder) {
2482 return null;
2483 }
2484 final bool backwardSelection =
2485 existingSelectionStart == null && existingSelectionEnd.offset == range.start ||
2486 existingSelectionStart == existingSelectionEnd &&
2487 existingSelectionEnd.offset == range.start ||
2488 existingSelectionStart != null &&
2489 existingSelectionStart.offset > existingSelectionEnd.offset;
2490 final _TextBoundaryRecord boundaryAtPositionRelativeToTargetParagraph = getTextBoundary(
2491 positionRelativeToTargetParagraph,
2492 targetText,
2493 );
2494 final TextPosition targetParagraphPlaceholderTextPosition = _getPositionInParagraph(
2495 targetParagraph,
2496 );
2497 final TextRange targetParagraphPlaceholderRange = TextRange(
2498 start: targetParagraphPlaceholderTextPosition.offset,
2499 end: targetParagraphPlaceholderTextPosition.offset + _placeholderLength,
2500 );
2501 if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset <
2502 targetParagraphPlaceholderRange.start &&
2503 boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset <
2504 targetParagraphPlaceholderRange.start) {
2505 _setSelectionPosition(TextPosition(offset: range.start), isEnd: isEnd);
2506 return SelectionResult.previous;
2507 }
2508 if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset >
2509 targetParagraphPlaceholderRange.end &&
2510 boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset >
2511 targetParagraphPlaceholderRange.end) {
2512 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2513 return SelectionResult.next;
2514 }
2515 if (backwardSelection) {
2516 if (boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset <=
2517 targetParagraphPlaceholderRange.end) {
2518 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2519 return SelectionResult.end;
2520 }
2521 if (boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset >
2522 targetParagraphPlaceholderRange.end) {
2523 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2524 return SelectionResult.next;
2525 }
2526 } else {
2527 if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset >=
2528 targetParagraphPlaceholderRange.start) {
2529 _setSelectionPosition(TextPosition(offset: range.start), isEnd: isEnd);
2530 return SelectionResult.end;
2531 }
2532 if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset <
2533 targetParagraphPlaceholderRange.start) {
2534 _setSelectionPosition(TextPosition(offset: range.start), isEnd: isEnd);
2535 return SelectionResult.previous;
2536 }
2537 }
2538 }
2539 }
2540 return null;
2541 }
2542
2543 // This method handles updating the end edge by a text boundary that may
2544 // not be contained within this selectable fragment. It is possible
2545 // that a boundary spans multiple selectable fragments when the text contains
2546 // [WidgetSpan]s.
2547 //
2548 // This method differs from [_updateSelectionEndEdgeByMultiSelectableBoundary]
2549 // in that to maintain the origin text boundary selected at a placeholder, this
2550 // selectable fragment must be aware of the [RenderParagraph] that closely
2551 // encompasses the complete origin text boundary.
2552 //
2553 // See [_updateSelectionStartEdgeAtPlaceholderByMultiSelectableTextBoundary]
2554 // for the method that handles updating the start edge.
2555 SelectionResult? _updateSelectionEndEdgeAtPlaceholderByMultiSelectableTextBoundary(
2556 _TextBoundaryAtPositionInText getTextBoundary,
2557 Offset globalPosition,
2558 bool paragraphContainsPosition,
2559 TextPosition position,
2560 TextPosition? existingSelectionStart,
2561 TextPosition? existingSelectionEnd,
2562 ) {
2563 const bool isEnd = true;
2564 if (_selectableContainsOriginTextBoundary &&
2565 existingSelectionStart != null &&
2566 existingSelectionEnd != null) {
2567 // If this selectable contains the origin boundary, maintain the existing
2568 // selection.
2569 final bool forwardSelection = existingSelectionEnd.offset >= existingSelectionStart.offset;
2570 final RenderParagraph originParagraph = _getOriginParagraph();
2571 final bool fragmentBelongsToOriginParagraph = originParagraph == paragraph;
2572 if (fragmentBelongsToOriginParagraph) {
2573 return _updateSelectionEndEdgeByMultiSelectableTextBoundary(
2574 getTextBoundary,
2575 paragraphContainsPosition,
2576 position,
2577 existingSelectionStart,
2578 existingSelectionEnd,
2579 );
2580 }
2581 final Matrix4 originTransform = originParagraph.getTransformTo(null);
2582 originTransform.invert();
2583 final Offset originParagraphLocalPosition = MatrixUtils.transformPoint(
2584 originTransform,
2585 globalPosition,
2586 );
2587 final bool positionWithinOriginParagraph = originParagraph.paintBounds.contains(
2588 originParagraphLocalPosition,
2589 );
2590 final TextPosition positionRelativeToOriginParagraph = originParagraph.getPositionForOffset(
2591 originParagraphLocalPosition,
2592 );
2593 if (positionWithinOriginParagraph) {
2594 // When the selection is inverted by the new position it is necessary to
2595 // swap the end edge (moving edge) with the start edge (static edge) to
2596 // maintain the origin text boundary within the selection.
2597 final String originText = originParagraph.text.toPlainText(includeSemanticsLabels: false);
2598 final _TextBoundaryRecord boundaryAtPosition = getTextBoundary(
2599 positionRelativeToOriginParagraph,
2600 originText,
2601 );
2602 final _TextBoundaryRecord originTextBoundary = getTextBoundary(
2603 _getPositionInParagraph(originParagraph),
2604 originText,
2605 );
2606 final TextPosition targetPosition;
2607 final int pivotOffset = forwardSelection
2608 ? originTextBoundary.boundaryStart.offset
2609 : originTextBoundary.boundaryEnd.offset;
2610 final bool shouldSwapEdges =
2611 !forwardSelection != (positionRelativeToOriginParagraph.offset < pivotOffset);
2612 if (positionRelativeToOriginParagraph.offset < pivotOffset) {
2613 targetPosition = boundaryAtPosition.boundaryStart;
2614 } else if (positionRelativeToOriginParagraph.offset > pivotOffset) {
2615 targetPosition = boundaryAtPosition.boundaryEnd;
2616 } else {
2617 // Keep the origin text boundary in bounds when position is at the static edge.
2618 targetPosition = existingSelectionEnd;
2619 }
2620 if (shouldSwapEdges) {
2621 _setSelectionPosition(existingSelectionEnd, isEnd: false);
2622 }
2623 _setSelectionPosition(_clampTextPosition(targetPosition), isEnd: isEnd);
2624 final bool finalSelectionIsForward =
2625 _textSelectionEnd!.offset >= _textSelectionStart!.offset;
2626 final TextPosition originParagraphPlaceholderTextPosition = _getPositionInParagraph(
2627 originParagraph,
2628 );
2629 final TextRange originParagraphPlaceholderRange = TextRange(
2630 start: originParagraphPlaceholderTextPosition.offset,
2631 end: originParagraphPlaceholderTextPosition.offset + _placeholderLength,
2632 );
2633 if (boundaryAtPosition.boundaryStart.offset > originParagraphPlaceholderRange.end &&
2634 boundaryAtPosition.boundaryEnd.offset > originParagraphPlaceholderRange.end) {
2635 return SelectionResult.next;
2636 }
2637 if (boundaryAtPosition.boundaryStart.offset < originParagraphPlaceholderRange.start &&
2638 boundaryAtPosition.boundaryEnd.offset < originParagraphPlaceholderRange.start) {
2639 return SelectionResult.previous;
2640 }
2641 if (finalSelectionIsForward) {
2642 if (boundaryAtPosition.boundaryEnd.offset <= originTextBoundary.boundaryEnd.offset) {
2643 return SelectionResult.end;
2644 }
2645 if (boundaryAtPosition.boundaryEnd.offset > originTextBoundary.boundaryEnd.offset) {
2646 return SelectionResult.next;
2647 }
2648 } else {
2649 if (boundaryAtPosition.boundaryStart.offset >= originTextBoundary.boundaryStart.offset) {
2650 return SelectionResult.end;
2651 }
2652 if (boundaryAtPosition.boundaryStart.offset < originTextBoundary.boundaryStart.offset) {
2653 return SelectionResult.previous;
2654 }
2655 }
2656 } else {
2657 // When the drag position is not contained within the origin paragraph,
2658 // swap the edges when the selection changes direction.
2659 //
2660 // [SelectionUtils.adjustDragOffset] will adjust the given [Offset] to the
2661 // beginning or end of the provided [Rect] based on whether the [Offset]
2662 // is located within the given [Rect].
2663 final Offset adjustedOffset = SelectionUtils.adjustDragOffset(
2664 originParagraph.paintBounds,
2665 originParagraphLocalPosition,
2666 direction: paragraph.textDirection,
2667 );
2668 final TextPosition adjustedPositionRelativeToOriginParagraph = originParagraph
2669 .getPositionForOffset(adjustedOffset);
2670 final TextPosition originParagraphPlaceholderTextPosition = _getPositionInParagraph(
2671 originParagraph,
2672 );
2673 final TextRange originParagraphPlaceholderRange = TextRange(
2674 start: originParagraphPlaceholderTextPosition.offset,
2675 end: originParagraphPlaceholderTextPosition.offset + _placeholderLength,
2676 );
2677 if (forwardSelection &&
2678 adjustedPositionRelativeToOriginParagraph.offset <=
2679 originParagraphPlaceholderRange.start) {
2680 _setSelectionPosition(existingSelectionEnd, isEnd: false);
2681 _setSelectionPosition(TextPosition(offset: range.start), isEnd: isEnd);
2682 return SelectionResult.previous;
2683 }
2684 if (!forwardSelection &&
2685 adjustedPositionRelativeToOriginParagraph.offset >=
2686 originParagraphPlaceholderRange.end) {
2687 _setSelectionPosition(existingSelectionEnd, isEnd: false);
2688 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2689 return SelectionResult.next;
2690 }
2691 if (forwardSelection &&
2692 adjustedPositionRelativeToOriginParagraph.offset >=
2693 originParagraphPlaceholderRange.end) {
2694 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2695 return SelectionResult.next;
2696 }
2697 if (!forwardSelection &&
2698 adjustedPositionRelativeToOriginParagraph.offset <=
2699 originParagraphPlaceholderRange.start) {
2700 _setSelectionPosition(TextPosition(offset: range.start), isEnd: isEnd);
2701 return SelectionResult.previous;
2702 }
2703 }
2704 } else {
2705 // When the drag position is somewhere on the root text and not a placeholder,
2706 // traverse the selectable fragments relative to the [RenderParagraph] that
2707 // contains the drag position.
2708 if (paragraphContainsPosition) {
2709 return _updateSelectionEndEdgeByMultiSelectableTextBoundary(
2710 getTextBoundary,
2711 paragraphContainsPosition,
2712 position,
2713 existingSelectionStart,
2714 existingSelectionEnd,
2715 );
2716 }
2717 if (existingSelectionStart != null) {
2718 final ({RenderParagraph paragraph, Offset localPosition})? targetDetails =
2719 _getParagraphContainingPosition(globalPosition);
2720 if (targetDetails == null) {
2721 return null;
2722 }
2723 final RenderParagraph targetParagraph = targetDetails.paragraph;
2724 final TextPosition positionRelativeToTargetParagraph = targetParagraph.getPositionForOffset(
2725 targetDetails.localPosition,
2726 );
2727 final String targetText = targetParagraph.text.toPlainText(includeSemanticsLabels: false);
2728 final bool positionOnPlaceholder =
2729 targetParagraph
2730 .getWordBoundary(positionRelativeToTargetParagraph)
2731 .textInside(targetText) ==
2732 _placeholderCharacter;
2733 if (positionOnPlaceholder) {
2734 return null;
2735 }
2736 final bool backwardSelection =
2737 existingSelectionEnd == null && existingSelectionStart.offset == range.end ||
2738 existingSelectionStart == existingSelectionEnd &&
2739 existingSelectionStart.offset == range.end ||
2740 existingSelectionEnd != null &&
2741 existingSelectionStart.offset > existingSelectionEnd.offset;
2742 final _TextBoundaryRecord boundaryAtPositionRelativeToTargetParagraph = getTextBoundary(
2743 positionRelativeToTargetParagraph,
2744 targetText,
2745 );
2746 final TextPosition targetParagraphPlaceholderTextPosition = _getPositionInParagraph(
2747 targetParagraph,
2748 );
2749 final TextRange targetParagraphPlaceholderRange = TextRange(
2750 start: targetParagraphPlaceholderTextPosition.offset,
2751 end: targetParagraphPlaceholderTextPosition.offset + _placeholderLength,
2752 );
2753 if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset <
2754 targetParagraphPlaceholderRange.start &&
2755 boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset <
2756 targetParagraphPlaceholderRange.start) {
2757 _setSelectionPosition(TextPosition(offset: range.start), isEnd: isEnd);
2758 return SelectionResult.previous;
2759 }
2760 if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset >
2761 targetParagraphPlaceholderRange.end &&
2762 boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset >
2763 targetParagraphPlaceholderRange.end) {
2764 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2765 return SelectionResult.next;
2766 }
2767 if (backwardSelection) {
2768 if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset >=
2769 targetParagraphPlaceholderRange.start) {
2770 _setSelectionPosition(TextPosition(offset: range.start), isEnd: isEnd);
2771 return SelectionResult.end;
2772 }
2773 if (boundaryAtPositionRelativeToTargetParagraph.boundaryStart.offset <
2774 targetParagraphPlaceholderRange.start) {
2775 _setSelectionPosition(TextPosition(offset: range.start), isEnd: isEnd);
2776 return SelectionResult.previous;
2777 }
2778 } else {
2779 if (boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset <=
2780 targetParagraphPlaceholderRange.end) {
2781 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2782 return SelectionResult.end;
2783 }
2784 if (boundaryAtPositionRelativeToTargetParagraph.boundaryEnd.offset >
2785 targetParagraphPlaceholderRange.end) {
2786 _setSelectionPosition(TextPosition(offset: range.end), isEnd: isEnd);
2787 return SelectionResult.next;
2788 }
2789 }
2790 }
2791 }
2792 return null;
2793 }
2794
2795 SelectionResult _updateSelectionEdgeByMultiSelectableTextBoundary(
2796 Offset globalPosition, {
2797 required bool isEnd,
2798 required _TextBoundaryAtPositionInText getTextBoundary,
2799 required _TextBoundaryAtPosition getClampedTextBoundary,
2800 }) {
2801 // When the start/end edges are swapped, i.e. the start is after the end, and
2802 // the scrollable synthesizes an event for the opposite edge, this will potentially
2803 // move the opposite edge outside of the origin text boundary and we are unable to recover.
2804 final TextPosition? existingSelectionStart = _textSelectionStart;
2805 final TextPosition? existingSelectionEnd = _textSelectionEnd;
2806
2807 _setSelectionPosition(null, isEnd: isEnd);
2808 final Matrix4 transform = paragraph.getTransformTo(null);
2809 transform.invert();
2810 final Offset localPosition = MatrixUtils.transformPoint(transform, globalPosition);
2811 if (_rect.isEmpty) {
2812 return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
2813 }
2814 final Offset adjustedOffset = SelectionUtils.adjustDragOffset(
2815 _rect,
2816 localPosition,
2817 direction: paragraph.textDirection,
2818 );
2819 final Offset adjustedOffsetRelativeToParagraph = SelectionUtils.adjustDragOffset(
2820 paragraph.paintBounds,
2821 localPosition,
2822 direction: paragraph.textDirection,
2823 );
2824
2825 final TextPosition position = paragraph.getPositionForOffset(adjustedOffset);
2826 final TextPosition positionInFullText = paragraph.getPositionForOffset(
2827 adjustedOffsetRelativeToParagraph,
2828 );
2829
2830 final SelectionResult? result;
2831 if (_isPlaceholder()) {
2832 result = isEnd
2833 ? _updateSelectionEndEdgeAtPlaceholderByMultiSelectableTextBoundary(
2834 getTextBoundary,
2835 globalPosition,
2836 paragraph.paintBounds.contains(localPosition),
2837 positionInFullText,
2838 existingSelectionStart,
2839 existingSelectionEnd,
2840 )
2841 : _updateSelectionStartEdgeAtPlaceholderByMultiSelectableTextBoundary(
2842 getTextBoundary,
2843 globalPosition,
2844 paragraph.paintBounds.contains(localPosition),
2845 positionInFullText,
2846 existingSelectionStart,
2847 existingSelectionEnd,
2848 );
2849 } else {
2850 result = isEnd
2851 ? _updateSelectionEndEdgeByMultiSelectableTextBoundary(
2852 getTextBoundary,
2853 paragraph.paintBounds.contains(localPosition),
2854 positionInFullText,
2855 existingSelectionStart,
2856 existingSelectionEnd,
2857 )
2858 : _updateSelectionStartEdgeByMultiSelectableTextBoundary(
2859 getTextBoundary,
2860 paragraph.paintBounds.contains(localPosition),
2861 positionInFullText,
2862 existingSelectionStart,
2863 existingSelectionEnd,
2864 );
2865 }
2866 if (result != null) {
2867 return result;
2868 }
2869
2870 // Check if the original local position is within the rect, if it is not then
2871 // we do not need to look up the text boundary for that position. This is to
2872 // maintain a selectables selection collapsed at 0 when the local position is
2873 // not located inside its rect.
2874 _TextBoundaryRecord? textBoundary = _boundingBoxesContains(localPosition)
2875 ? getClampedTextBoundary(position)
2876 : null;
2877 if (textBoundary != null &&
2878 (textBoundary.boundaryStart.offset < range.start &&
2879 textBoundary.boundaryEnd.offset <= range.start ||
2880 textBoundary.boundaryStart.offset >= range.end &&
2881 textBoundary.boundaryEnd.offset > range.end)) {
2882 // When the position is located at a placeholder inside of the text, then we may compute
2883 // a text boundary that does not belong to the current selectable fragment. In this case
2884 // we should invalidate the text boundary so that it is not taken into account when
2885 // computing the target position.
2886 textBoundary = null;
2887 }
2888 final TextPosition targetPosition = _clampTextPosition(
2889 isEnd
2890 ? _updateSelectionEndEdgeByTextBoundary(
2891 textBoundary,
2892 getClampedTextBoundary,
2893 position,
2894 existingSelectionStart,
2895 existingSelectionEnd,
2896 )
2897 : _updateSelectionStartEdgeByTextBoundary(
2898 textBoundary,
2899 getClampedTextBoundary,
2900 position,
2901 existingSelectionStart,
2902 existingSelectionEnd,
2903 ),
2904 );
2905
2906 _setSelectionPosition(targetPosition, isEnd: isEnd);
2907 if (targetPosition.offset == range.end) {
2908 return SelectionResult.next;
2909 }
2910
2911 if (targetPosition.offset == range.start) {
2912 return SelectionResult.previous;
2913 }
2914 // TODO(chunhtai): The geometry information should not be used to determine
2915 // selection result. This is a workaround to RenderParagraph, where it does
2916 // not have a way to get accurate text length if its text is truncated due to
2917 // layout constraint.
2918 return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
2919 }
2920
2921 TextPosition _closestTextBoundary(_TextBoundaryRecord textBoundary, TextPosition position) {
2922 final int differenceA = (position.offset - textBoundary.boundaryStart.offset).abs();
2923 final int differenceB = (position.offset - textBoundary.boundaryEnd.offset).abs();
2924 return differenceA < differenceB ? textBoundary.boundaryStart : textBoundary.boundaryEnd;
2925 }
2926
2927 bool _isPlaceholder() {
2928 // Determine whether this selectable fragment is a placeholder.
2929 RenderObject? current = paragraph.parent;
2930 while (current != null) {
2931 if (current is RenderParagraph) {
2932 return true;
2933 }
2934 current = current.parent;
2935 }
2936 return false;
2937 }
2938
2939 RenderParagraph _getOriginParagraph() {
2940 // This method should only be called from a fragment that contains
2941 // the origin boundary. By traversing up the RenderTree, determine the
2942 // highest RenderParagraph that contains the origin text boundary.
2943 assert(_selectableContainsOriginTextBoundary);
2944 // Begin at the parent because it is guaranteed the paragraph containing
2945 // this selectable fragment contains the origin boundary.
2946 RenderObject? current = paragraph.parent;
2947 RenderParagraph? originParagraph;
2948 while (current != null) {
2949 if (current is RenderParagraph) {
2950 if (current._lastSelectableFragments != null) {
2951 bool paragraphContainsOriginTextBoundary = false;
2952 for (final _SelectableFragment fragment in current._lastSelectableFragments!) {
2953 if (fragment._selectableContainsOriginTextBoundary) {
2954 paragraphContainsOriginTextBoundary = true;
2955 originParagraph = current;
2956 break;
2957 }
2958 }
2959 if (!paragraphContainsOriginTextBoundary) {
2960 return originParagraph ?? paragraph;
2961 }
2962 }
2963 }
2964 current = current.parent;
2965 }
2966 return originParagraph ?? paragraph;
2967 }
2968
2969 ({RenderParagraph paragraph, Offset localPosition})? _getParagraphContainingPosition(
2970 Offset globalPosition,
2971 ) {
2972 // This method will return the closest [RenderParagraph] whose rect
2973 // contains the given `globalPosition` and the given `globalPosition`
2974 // relative to that [RenderParagraph]. If no ancestor [RenderParagraph]
2975 // contains the given `globalPosition` then this method will return null.
2976 RenderObject? current = paragraph;
2977 while (current != null) {
2978 if (current is RenderParagraph) {
2979 final Matrix4 currentTransform = current.getTransformTo(null);
2980 currentTransform.invert();
2981 final Offset currentParagraphLocalPosition = MatrixUtils.transformPoint(
2982 currentTransform,
2983 globalPosition,
2984 );
2985 final bool positionWithinCurrentParagraph = current.paintBounds.contains(
2986 currentParagraphLocalPosition,
2987 );
2988 if (positionWithinCurrentParagraph) {
2989 return (paragraph: current, localPosition: currentParagraphLocalPosition);
2990 }
2991 }
2992 current = current.parent;
2993 }
2994 return null;
2995 }
2996
2997 bool _boundingBoxesContains(Offset position) {
2998 for (final Rect rect in boundingBoxes) {
2999 if (rect.contains(position)) {
3000 return true;
3001 }
3002 }
3003 return false;
3004 }
3005
3006 TextPosition _clampTextPosition(TextPosition position) {
3007 // Affinity of range.end is upstream.
3008 if (position.offset > range.end ||
3009 (position.offset == range.end && position.affinity == TextAffinity.downstream)) {
3010 return TextPosition(offset: range.end, affinity: TextAffinity.upstream);
3011 }
3012 if (position.offset < range.start) {
3013 return TextPosition(offset: range.start);
3014 }
3015 return position;
3016 }
3017
3018 void _setSelectionPosition(TextPosition? position, {required bool isEnd}) {
3019 if (isEnd) {
3020 _textSelectionEnd = position;
3021 } else {
3022 _textSelectionStart = position;
3023 }
3024 }
3025
3026 SelectionResult _handleClearSelection() {
3027 _textSelectionStart = null;
3028 _textSelectionEnd = null;
3029 _selectableContainsOriginTextBoundary = false;
3030 return SelectionResult.none;
3031 }
3032
3033 SelectionResult _handleSelectAll() {
3034 _textSelectionStart = TextPosition(offset: range.start);
3035 _textSelectionEnd = TextPosition(offset: range.end, affinity: TextAffinity.upstream);
3036 return SelectionResult.none;
3037 }
3038
3039 SelectionResult _handleSelectTextBoundary(_TextBoundaryRecord textBoundary) {
3040 // This fragment may not contain the boundary, decide what direction the target
3041 // fragment is located in. Because fragments are separated by placeholder
3042 // spans, we also check if the beginning or end of the boundary is touching
3043 // either edge of this fragment.
3044 if (textBoundary.boundaryStart.offset < range.start &&
3045 textBoundary.boundaryEnd.offset <= range.start) {
3046 return SelectionResult.previous;
3047 } else if (textBoundary.boundaryStart.offset >= range.end &&
3048 textBoundary.boundaryEnd.offset > range.end) {
3049 return SelectionResult.next;
3050 }
3051 // Fragments are separated by placeholder span, the text boundary shouldn't
3052 // expand across fragments.
3053 assert(
3054 textBoundary.boundaryStart.offset >= range.start &&
3055 textBoundary.boundaryEnd.offset <= range.end,
3056 );
3057 _textSelectionStart = textBoundary.boundaryStart;
3058 _textSelectionEnd = textBoundary.boundaryEnd;
3059 _selectableContainsOriginTextBoundary = true;
3060 return SelectionResult.end;
3061 }
3062
3063 TextRange? _intersect(TextRange a, TextRange b) {
3064 assert(a.isNormalized);
3065 assert(b.isNormalized);
3066 final int startMax = math.max(a.start, b.start);
3067 final int endMin = math.min(a.end, b.end);
3068 if (startMax <= endMin) {
3069 // Intersection.
3070 return TextRange(start: startMax, end: endMin);
3071 }
3072 return null;
3073 }
3074
3075 SelectionResult _handleSelectMultiFragmentTextBoundary(_TextBoundaryRecord textBoundary) {
3076 // This fragment may not contain the boundary, decide what direction the target
3077 // fragment is located in. Because fragments are separated by placeholder
3078 // spans, we also check if the beginning or end of the boundary is touching
3079 // either edge of this fragment.
3080 if (textBoundary.boundaryStart.offset < range.start &&
3081 textBoundary.boundaryEnd.offset <= range.start) {
3082 return SelectionResult.previous;
3083 } else if (textBoundary.boundaryStart.offset >= range.end &&
3084 textBoundary.boundaryEnd.offset > range.end) {
3085 return SelectionResult.next;
3086 }
3087 final TextRange boundaryAsRange = TextRange(
3088 start: textBoundary.boundaryStart.offset,
3089 end: textBoundary.boundaryEnd.offset,
3090 );
3091 final TextRange? intersectRange = _intersect(range, boundaryAsRange);
3092 if (intersectRange != null) {
3093 _textSelectionStart = TextPosition(offset: intersectRange.start);
3094 _textSelectionEnd = TextPosition(offset: intersectRange.end);
3095 _selectableContainsOriginTextBoundary = true;
3096 if (range.end < textBoundary.boundaryEnd.offset) {
3097 return SelectionResult.next;
3098 }
3099 return SelectionResult.end;
3100 }
3101 return SelectionResult.none;
3102 }
3103
3104 _TextBoundaryRecord _adjustTextBoundaryAtPosition(TextRange textBoundary, TextPosition position) {
3105 late final TextPosition start;
3106 late final TextPosition end;
3107 if (position.offset > textBoundary.end) {
3108 start = end = TextPosition(offset: position.offset);
3109 } else {
3110 start = TextPosition(offset: textBoundary.start);
3111 end = TextPosition(offset: textBoundary.end, affinity: TextAffinity.upstream);
3112 }
3113 return (boundaryStart: start, boundaryEnd: end);
3114 }
3115
3116 SelectionResult _handleSelectWord(Offset globalPosition) {
3117 final TextPosition position = paragraph.getPositionForOffset(
3118 paragraph.globalToLocal(globalPosition),
3119 );
3120 if (_positionIsWithinCurrentSelection(position) && _textSelectionStart != _textSelectionEnd) {
3121 return SelectionResult.end;
3122 }
3123 final _TextBoundaryRecord wordBoundary = _getWordBoundaryAtPosition(position);
3124 return _handleSelectTextBoundary(wordBoundary);
3125 }
3126
3127 _TextBoundaryRecord _getWordBoundaryAtPosition(TextPosition position) {
3128 final TextRange word = paragraph.getWordBoundary(position);
3129 assert(word.isNormalized);
3130 return _adjustTextBoundaryAtPosition(word, position);
3131 }
3132
3133 SelectionResult _handleSelectParagraph(Offset globalPosition) {
3134 final Offset localPosition = paragraph.globalToLocal(globalPosition);
3135 final TextPosition position = paragraph.getPositionForOffset(localPosition);
3136 final _TextBoundaryRecord paragraphBoundary = _getParagraphBoundaryAtPosition(
3137 position,
3138 fullText,
3139 );
3140 return _handleSelectMultiFragmentTextBoundary(paragraphBoundary);
3141 }
3142
3143 TextPosition _getPositionInParagraph(RenderParagraph targetParagraph) {
3144 final Matrix4 transform = paragraph.getTransformTo(targetParagraph);
3145 final Offset localCenter = paragraph.paintBounds.centerLeft;
3146 final Offset localPos = MatrixUtils.transformPoint(transform, localCenter);
3147 final TextPosition position = targetParagraph.getPositionForOffset(localPos);
3148 return position;
3149 }
3150
3151 _TextBoundaryRecord _getParagraphBoundaryAtPosition(TextPosition position, String text) {
3152 final ParagraphBoundary paragraphBoundary = ParagraphBoundary(text);
3153 // Use position.offset - 1 when `position` is at the end of the selectable to retrieve
3154 // the previous text boundary's location.
3155 final int paragraphStart =
3156 paragraphBoundary.getLeadingTextBoundaryAt(
3157 position.offset == text.length || position.affinity == TextAffinity.upstream
3158 ? position.offset - 1
3159 : position.offset,
3160 ) ??
3161 0;
3162 final int paragraphEnd =
3163 paragraphBoundary.getTrailingTextBoundaryAt(position.offset) ?? text.length;
3164 final TextRange paragraphRange = TextRange(start: paragraphStart, end: paragraphEnd);
3165 assert(paragraphRange.isNormalized);
3166 return _adjustTextBoundaryAtPosition(paragraphRange, position);
3167 }
3168
3169 _TextBoundaryRecord _getClampedParagraphBoundaryAtPosition(TextPosition position) {
3170 final ParagraphBoundary paragraphBoundary = ParagraphBoundary(fullText);
3171 // Use position.offset - 1 when `position` is at the end of the selectable to retrieve
3172 // the previous text boundary's location.
3173 int paragraphStart =
3174 paragraphBoundary.getLeadingTextBoundaryAt(
3175 position.offset == fullText.length || position.affinity == TextAffinity.upstream
3176 ? position.offset - 1
3177 : position.offset,
3178 ) ??
3179 0;
3180 int paragraphEnd =
3181 paragraphBoundary.getTrailingTextBoundaryAt(position.offset) ?? fullText.length;
3182 paragraphStart = paragraphStart < range.start
3183 ? range.start
3184 : paragraphStart > range.end
3185 ? range.end
3186 : paragraphStart;
3187 paragraphEnd = paragraphEnd > range.end
3188 ? range.end
3189 : paragraphEnd < range.start
3190 ? range.start
3191 : paragraphEnd;
3192 final TextRange paragraphRange = TextRange(start: paragraphStart, end: paragraphEnd);
3193 assert(paragraphRange.isNormalized);
3194 return _adjustTextBoundaryAtPosition(paragraphRange, position);
3195 }
3196
3197 SelectionResult _handleDirectionallyExtendSelection(
3198 double horizontalBaseline,
3199 bool isExtent,
3200 SelectionExtendDirection movement,
3201 ) {
3202 final Matrix4 transform = paragraph.getTransformTo(null);
3203 if (transform.invert() == 0.0) {
3204 switch (movement) {
3205 case SelectionExtendDirection.previousLine:
3206 case SelectionExtendDirection.backward:
3207 return SelectionResult.previous;
3208 case SelectionExtendDirection.nextLine:
3209 case SelectionExtendDirection.forward:
3210 return SelectionResult.next;
3211 }
3212 }
3213 final double baselineInParagraphCoordinates = MatrixUtils.transformPoint(
3214 transform,
3215 Offset(horizontalBaseline, 0),
3216 ).dx;
3217 assert(!baselineInParagraphCoordinates.isNaN);
3218 final TextPosition newPosition;
3219 final SelectionResult result;
3220 switch (movement) {
3221 case SelectionExtendDirection.previousLine:
3222 case SelectionExtendDirection.nextLine:
3223 assert(_textSelectionEnd != null && _textSelectionStart != null);
3224 final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart!;
3225 final MapEntry<TextPosition, SelectionResult> moveResult = _handleVerticalMovement(
3226 targetedEdge,
3227 horizontalBaselineInParagraphCoordinates: baselineInParagraphCoordinates,
3228 below: movement == SelectionExtendDirection.nextLine,
3229 );
3230 newPosition = moveResult.key;
3231 result = moveResult.value;
3232 case SelectionExtendDirection.forward:
3233 case SelectionExtendDirection.backward:
3234 _textSelectionEnd ??= movement == SelectionExtendDirection.forward
3235 ? TextPosition(offset: range.start)
3236 : TextPosition(offset: range.end, affinity: TextAffinity.upstream);
3237 _textSelectionStart ??= _textSelectionEnd;
3238 final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart!;
3239 final Offset edgeOffsetInParagraphCoordinates = paragraph._getOffsetForPosition(
3240 targetedEdge,
3241 );
3242 final Offset baselineOffsetInParagraphCoordinates = Offset(
3243 baselineInParagraphCoordinates,
3244 // Use half of line height to point to the middle of the line.
3245 edgeOffsetInParagraphCoordinates.dy - paragraph._textPainter.preferredLineHeight / 2,
3246 );
3247 newPosition = paragraph.getPositionForOffset(baselineOffsetInParagraphCoordinates);
3248 result = SelectionResult.end;
3249 }
3250 if (isExtent) {
3251 _textSelectionEnd = newPosition;
3252 } else {
3253 _textSelectionStart = newPosition;
3254 }
3255 return result;
3256 }
3257
3258 SelectionResult _handleGranularlyExtendSelection(
3259 bool forward,
3260 bool isExtent,
3261 TextGranularity granularity,
3262 ) {
3263 _textSelectionEnd ??= forward
3264 ? TextPosition(offset: range.start)
3265 : TextPosition(offset: range.end, affinity: TextAffinity.upstream);
3266 _textSelectionStart ??= _textSelectionEnd;
3267 final TextPosition targetedEdge = isExtent ? _textSelectionEnd! : _textSelectionStart!;
3268 if (forward && (targetedEdge.offset == range.end)) {
3269 return SelectionResult.next;
3270 }
3271 if (!forward && (targetedEdge.offset == range.start)) {
3272 return SelectionResult.previous;
3273 }
3274 final SelectionResult result;
3275 final TextPosition newPosition;
3276 switch (granularity) {
3277 case TextGranularity.character:
3278 final String text = range.textInside(fullText);
3279 newPosition = _moveBeyondTextBoundaryAtDirection(
3280 targetedEdge,
3281 forward,
3282 CharacterBoundary(text),
3283 );
3284 result = SelectionResult.end;
3285 case TextGranularity.word:
3286 final TextBoundary textBoundary = paragraph._textPainter.wordBoundaries.moveByWordBoundary;
3287 newPosition = _moveBeyondTextBoundaryAtDirection(targetedEdge, forward, textBoundary);
3288 result = SelectionResult.end;
3289 case TextGranularity.paragraph:
3290 final String text = range.textInside(fullText);
3291 newPosition = _moveBeyondTextBoundaryAtDirection(
3292 targetedEdge,
3293 forward,
3294 ParagraphBoundary(text),
3295 );
3296 result = SelectionResult.end;
3297 case TextGranularity.line:
3298 newPosition = _moveToTextBoundaryAtDirection(targetedEdge, forward, LineBoundary(this));
3299 result = SelectionResult.end;
3300 case TextGranularity.document:
3301 final String text = range.textInside(fullText);
3302 newPosition = _moveBeyondTextBoundaryAtDirection(
3303 targetedEdge,
3304 forward,
3305 DocumentBoundary(text),
3306 );
3307 if (forward && newPosition.offset == range.end) {
3308 result = SelectionResult.next;
3309 } else if (!forward && newPosition.offset == range.start) {
3310 result = SelectionResult.previous;
3311 } else {
3312 result = SelectionResult.end;
3313 }
3314 }
3315
3316 if (isExtent) {
3317 _textSelectionEnd = newPosition;
3318 } else {
3319 _textSelectionStart = newPosition;
3320 }
3321 return result;
3322 }
3323
3324 // Move **beyond** the local boundary of the given type (unless range.start or
3325 // range.end is reached). Used for most TextGranularity types except for
3326 // TextGranularity.line, to ensure the selection movement doesn't get stuck at
3327 // a local fixed point.
3328 TextPosition _moveBeyondTextBoundaryAtDirection(
3329 TextPosition end,
3330 bool forward,
3331 TextBoundary textBoundary,
3332 ) {
3333 final int newOffset = forward
3334 ? textBoundary.getTrailingTextBoundaryAt(end.offset) ?? range.end
3335 : textBoundary.getLeadingTextBoundaryAt(end.offset - 1) ?? range.start;
3336 return TextPosition(offset: newOffset);
3337 }
3338
3339 // Move **to** the local boundary of the given type. Typically used for line
3340 // boundaries, such that performing "move to line start" more than once never
3341 // moves the selection to the previous line.
3342 TextPosition _moveToTextBoundaryAtDirection(
3343 TextPosition end,
3344 bool forward,
3345 TextBoundary textBoundary,
3346 ) {
3347 assert(end.offset >= 0);
3348 final int caretOffset;
3349 switch (end.affinity) {
3350 case TextAffinity.upstream:
3351 if (end.offset < 1 && !forward) {
3352 assert(end.offset == 0);
3353 return const TextPosition(offset: 0);
3354 }
3355 final CharacterBoundary characterBoundary = CharacterBoundary(fullText);
3356 caretOffset =
3357 math.max(
3358 0,
3359 characterBoundary.getLeadingTextBoundaryAt(range.start + end.offset) ?? range.start,
3360 ) -
3361 1;
3362 case TextAffinity.downstream:
3363 caretOffset = end.offset;
3364 }
3365 final int offset = forward
3366 ? textBoundary.getTrailingTextBoundaryAt(caretOffset) ?? range.end
3367 : textBoundary.getLeadingTextBoundaryAt(caretOffset) ?? range.start;
3368 return TextPosition(offset: offset);
3369 }
3370
3371 MapEntry<TextPosition, SelectionResult> _handleVerticalMovement(
3372 TextPosition position, {
3373 required double horizontalBaselineInParagraphCoordinates,
3374 required bool below,
3375 }) {
3376 final List<ui.LineMetrics> lines = paragraph._textPainter.computeLineMetrics();
3377 final Offset offset = paragraph.getOffsetForCaret(position, Rect.zero);
3378 int currentLine = lines.length - 1;
3379 for (final ui.LineMetrics lineMetrics in lines) {
3380 if (lineMetrics.baseline > offset.dy) {
3381 currentLine = lineMetrics.lineNumber;
3382 break;
3383 }
3384 }
3385 final TextPosition newPosition;
3386 if (below && currentLine == lines.length - 1) {
3387 newPosition = TextPosition(offset: range.end, affinity: TextAffinity.upstream);
3388 } else if (!below && currentLine == 0) {
3389 newPosition = TextPosition(offset: range.start);
3390 } else {
3391 final int newLine = below ? currentLine + 1 : currentLine - 1;
3392 newPosition = _clampTextPosition(
3393 paragraph.getPositionForOffset(
3394 Offset(horizontalBaselineInParagraphCoordinates, lines[newLine].baseline),
3395 ),
3396 );
3397 }
3398 final SelectionResult result;
3399 if (newPosition.offset == range.start) {
3400 result = SelectionResult.previous;
3401 } else if (newPosition.offset == range.end) {
3402 result = SelectionResult.next;
3403 } else {
3404 result = SelectionResult.end;
3405 }
3406 assert(result != SelectionResult.next || below);
3407 assert(result != SelectionResult.previous || !below);
3408 return MapEntry<TextPosition, SelectionResult>(newPosition, result);
3409 }
3410
3411 /// Whether the given text position is contained in current selection
3412 /// range.
3413 ///
3414 /// The parameter `start` must be smaller than `end`.
3415 bool _positionIsWithinCurrentSelection(TextPosition position) {
3416 if (_textSelectionStart == null || _textSelectionEnd == null) {
3417 return false;
3418 }
3419 // Normalize current selection.
3420 late TextPosition currentStart;
3421 late TextPosition currentEnd;
3422 if (_compareTextPositions(_textSelectionStart!, _textSelectionEnd!) > 0) {
3423 currentStart = _textSelectionStart!;
3424 currentEnd = _textSelectionEnd!;
3425 } else {
3426 currentStart = _textSelectionEnd!;
3427 currentEnd = _textSelectionStart!;
3428 }
3429 return _compareTextPositions(currentStart, position) >= 0 &&
3430 _compareTextPositions(currentEnd, position) <= 0;
3431 }
3432
3433 /// Compares two text positions.
3434 ///
3435 /// Returns 1 if `position` < `otherPosition`, -1 if `position` > `otherPosition`,
3436 /// or 0 if they are equal.
3437 static int _compareTextPositions(TextPosition position, TextPosition otherPosition) {
3438 if (position.offset < otherPosition.offset) {
3439 return 1;
3440 } else if (position.offset > otherPosition.offset) {
3441 return -1;
3442 } else if (position.affinity == otherPosition.affinity) {
3443 return 0;
3444 } else {
3445 return position.affinity == TextAffinity.upstream ? 1 : -1;
3446 }
3447 }
3448
3449 @override
3450 Matrix4 getTransformTo(RenderObject? ancestor) {
3451 return paragraph.getTransformTo(ancestor);
3452 }
3453
3454 @override
3455 void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) {
3456 if (!paragraph.attached) {
3457 assert(startHandle == null && endHandle == null, 'Only clean up can be called.');
3458 return;
3459 }
3460 if (_startHandleLayerLink != startHandle) {
3461 _startHandleLayerLink = startHandle;
3462 paragraph.markNeedsPaint();
3463 }
3464 if (_endHandleLayerLink != endHandle) {
3465 _endHandleLayerLink = endHandle;
3466 paragraph.markNeedsPaint();
3467 }
3468 }
3469
3470 List<Rect>? _cachedBoundingBoxes;
3471 @override
3472 List<Rect> get boundingBoxes {
3473 if (_cachedBoundingBoxes == null) {
3474 final List<TextBox> boxes = paragraph.getBoxesForSelection(
3475 TextSelection(baseOffset: range.start, extentOffset: range.end),
3476 boxHeightStyle: ui.BoxHeightStyle.max,
3477 );
3478 if (boxes.isNotEmpty) {
3479 _cachedBoundingBoxes = <Rect>[];
3480 for (final TextBox textBox in boxes) {
3481 _cachedBoundingBoxes!.add(textBox.toRect());
3482 }
3483 } else {
3484 final Offset offset = paragraph._getOffsetForPosition(TextPosition(offset: range.start));
3485 final Rect rect = Rect.fromPoints(
3486 offset,
3487 offset.translate(0, -paragraph._textPainter.preferredLineHeight),
3488 );
3489 _cachedBoundingBoxes = <Rect>[rect];
3490 }
3491 }
3492 return _cachedBoundingBoxes!;
3493 }
3494
3495 Rect? _cachedRect;
3496 Rect get _rect {
3497 if (_cachedRect == null) {
3498 final List<TextBox> boxes = paragraph.getBoxesForSelection(
3499 TextSelection(baseOffset: range.start, extentOffset: range.end),
3500 );
3501 if (boxes.isNotEmpty) {
3502 Rect result = boxes.first.toRect();
3503 for (int index = 1; index < boxes.length; index += 1) {
3504 result = result.expandToInclude(boxes[index].toRect());
3505 }
3506 _cachedRect = result;
3507 } else {
3508 final Offset offset = paragraph._getOffsetForPosition(TextPosition(offset: range.start));
3509 _cachedRect = Rect.fromPoints(
3510 offset,
3511 offset.translate(0, -paragraph._textPainter.preferredLineHeight),
3512 );
3513 }
3514 }
3515 return _cachedRect!;
3516 }
3517
3518 void didChangeParagraphLayout() {
3519 _cachedRect = null;
3520 _cachedBoundingBoxes = null;
3521 }
3522
3523 @override
3524 int get contentLength => range.end - range.start;
3525
3526 @override
3527 Size get size {
3528 return _rect.size;
3529 }
3530
3531 void paint(PaintingContext context, Offset offset) {
3532 if (_textSelectionStart == null || _textSelectionEnd == null) {
3533 return;
3534 }
3535 if (paragraph.selectionColor != null) {
3536 final TextSelection selection = TextSelection(
3537 baseOffset: _textSelectionStart!.offset,
3538 extentOffset: _textSelectionEnd!.offset,
3539 );
3540 final Paint selectionPaint = Paint()
3541 ..style = PaintingStyle.fill
3542 ..color = paragraph.selectionColor!;
3543 for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) {
3544 context.canvas.drawRect(textBox.toRect().shift(offset), selectionPaint);
3545 }
3546 }
3547 if (_startHandleLayerLink != null && value.startSelectionPoint != null) {
3548 context.pushLayer(
3549 LeaderLayer(
3550 link: _startHandleLayerLink!,
3551 offset: offset + value.startSelectionPoint!.localPosition,
3552 ),
3553 (PaintingContext context, Offset offset) {},
3554 Offset.zero,
3555 );
3556 }
3557 if (_endHandleLayerLink != null && value.endSelectionPoint != null) {
3558 context.pushLayer(
3559 LeaderLayer(
3560 link: _endHandleLayerLink!,
3561 offset: offset + value.endSelectionPoint!.localPosition,
3562 ),
3563 (PaintingContext context, Offset offset) {},
3564 Offset.zero,
3565 );
3566 }
3567 }
3568
3569 @override
3570 TextSelection getLineAtOffset(TextPosition position) {
3571 final TextRange line = paragraph._getLineAtOffset(position);
3572 final int start = line.start.clamp(range.start, range.end);
3573 final int end = line.end.clamp(range.start, range.end);
3574 return TextSelection(baseOffset: start, extentOffset: end);
3575 }
3576
3577 @override
3578 TextPosition getTextPositionAbove(TextPosition position) {
3579 return _clampTextPosition(paragraph._getTextPositionAbove(position));
3580 }
3581
3582 @override
3583 TextPosition getTextPositionBelow(TextPosition position) {
3584 return _clampTextPosition(paragraph._getTextPositionBelow(position));
3585 }
3586
3587 @override
3588 TextRange getWordBoundary(TextPosition position) => paragraph.getWordBoundary(position);
3589
3590 @override
3591 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
3592 super.debugFillProperties(properties);
3593 properties.add(DiagnosticsProperty<String>('textInsideRange', range.textInside(fullText)));
3594 properties.add(DiagnosticsProperty<TextRange>('range', range));
3595 properties.add(DiagnosticsProperty<String>('fullText', fullText));
3596 }
3597}
3598