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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com