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