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/gestures.dart';
6/// @docImport 'package:flutter/material.dart';
7///
8/// @docImport 'editable_text.dart';
9/// @docImport 'gesture_detector.dart';
10/// @docImport 'implicit_animations.dart';
11/// @docImport 'transitions.dart';
12/// @docImport 'widget_span.dart';
13library;
14
15import 'dart:math';
16import 'dart:ui' as ui show TextHeightBehavior;
17
18import 'package:flutter/foundation.dart';
19import 'package:flutter/rendering.dart';
20
21import 'basic.dart';
22import 'default_selection_style.dart';
23import 'framework.dart';
24import 'inherited_theme.dart';
25import 'media_query.dart';
26import 'selectable_region.dart';
27import 'selection_container.dart';
28
29// Examples can assume:
30// late String _name;
31// late BuildContext context;
32
33/// The text style to apply to descendant [Text] widgets which don't have an
34/// explicit style.
35///
36/// {@tool dartpad}
37/// This example shows how to use [DefaultTextStyle.merge] to create a default
38/// text style that inherits styling information from the current default text
39/// style and overrides some properties.
40///
41/// ** See code in examples/api/lib/widgets/text/text.0.dart **
42/// {@end-tool}
43///
44/// See also:
45///
46/// * [AnimatedDefaultTextStyle], which animates changes in the text style
47/// smoothly over a given duration.
48/// * [DefaultTextStyleTransition], which takes a provided [Animation] to
49/// animate changes in text style smoothly over time.
50class DefaultTextStyle extends InheritedTheme {
51 /// Creates a default text style for the given subtree.
52 ///
53 /// Consider using [DefaultTextStyle.merge] to inherit styling information
54 /// from the current default text style for a given [BuildContext].
55 ///
56 /// The [maxLines] property may be null (and indeed defaults to null), but if
57 /// it is not null, it must be greater than zero.
58 const DefaultTextStyle({
59 super.key,
60 required this.style,
61 this.textAlign,
62 this.softWrap = true,
63 this.overflow = TextOverflow.clip,
64 this.maxLines,
65 this.textWidthBasis = TextWidthBasis.parent,
66 this.textHeightBehavior,
67 required super.child,
68 }) : assert(maxLines == null || maxLines > 0);
69
70 /// A const-constructable default text style that provides fallback values.
71 ///
72 /// Returned from [of] when the given [BuildContext] doesn't have an enclosing default text style.
73 ///
74 /// This constructor creates a [DefaultTextStyle] with an invalid [child], which
75 /// means the constructed value cannot be incorporated into the tree.
76 const DefaultTextStyle.fallback({super.key})
77 : style = const TextStyle(),
78 textAlign = null,
79 softWrap = true,
80 maxLines = null,
81 overflow = TextOverflow.clip,
82 textWidthBasis = TextWidthBasis.parent,
83 textHeightBehavior = null,
84 super(child: const _NullWidget());
85
86 /// Creates a default text style that overrides the text styles in scope at
87 /// this point in the widget tree.
88 ///
89 /// The given [style] is merged with the [style] from the default text style
90 /// for the [BuildContext] where the widget is inserted, and any of the other
91 /// arguments that are not null replace the corresponding properties on that
92 /// same default text style.
93 ///
94 /// This constructor cannot be used to override the [maxLines] property of the
95 /// ancestor with the value null, since null here is used to mean "defer to
96 /// ancestor". To replace a non-null [maxLines] from an ancestor with the null
97 /// value (to remove the restriction on number of lines), manually obtain the
98 /// ambient [DefaultTextStyle] using [DefaultTextStyle.of], then create a new
99 /// [DefaultTextStyle] using the [DefaultTextStyle.new] constructor directly.
100 /// See the source below for an example of how to do this (since that's
101 /// essentially what this constructor does).
102 ///
103 /// If a [textHeightBehavior] is provided, the existing configuration will be
104 /// replaced completely. To retain part of the original [textHeightBehavior],
105 /// manually obtain the ambient [DefaultTextStyle] using [DefaultTextStyle.of].
106 static Widget merge({
107 Key? key,
108 TextStyle? style,
109 TextAlign? textAlign,
110 bool? softWrap,
111 TextOverflow? overflow,
112 int? maxLines,
113 TextWidthBasis? textWidthBasis,
114 TextHeightBehavior? textHeightBehavior,
115 required Widget child,
116 }) {
117 return Builder(
118 builder: (BuildContext context) {
119 final DefaultTextStyle parent = DefaultTextStyle.of(context);
120 return DefaultTextStyle(
121 key: key,
122 style: parent.style.merge(style),
123 textAlign: textAlign ?? parent.textAlign,
124 softWrap: softWrap ?? parent.softWrap,
125 overflow: overflow ?? parent.overflow,
126 maxLines: maxLines ?? parent.maxLines,
127 textWidthBasis: textWidthBasis ?? parent.textWidthBasis,
128 textHeightBehavior: textHeightBehavior ?? parent.textHeightBehavior,
129 child: child,
130 );
131 },
132 );
133 }
134
135 /// The text style to apply.
136 final TextStyle style;
137
138 /// How each line of text in the Text widget should be aligned horizontally.
139 final TextAlign? textAlign;
140
141 /// Whether the text should break at soft line breaks.
142 ///
143 /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
144 ///
145 /// This also decides the [overflow] property's behavior. If this is true or null,
146 /// the glyph causing overflow, and those that follow, will not be rendered.
147 final bool softWrap;
148
149 /// How visual overflow should be handled.
150 ///
151 /// If [softWrap] is true or null, the glyph causing overflow, and those that follow,
152 /// will not be rendered. Otherwise, it will be shown with the given overflow option.
153 final TextOverflow overflow;
154
155 /// An optional maximum number of lines for the text to span, wrapping if necessary.
156 /// If the text exceeds the given number of lines, it will be truncated according
157 /// to [overflow].
158 ///
159 /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
160 /// edge of the box.
161 ///
162 /// If this is non-null, it will override even explicit null values of
163 /// [Text.maxLines].
164 final int? maxLines;
165
166 /// The strategy to use when calculating the width of the Text.
167 ///
168 /// See [TextWidthBasis] for possible values and their implications.
169 final TextWidthBasis textWidthBasis;
170
171 /// {@macro dart.ui.textHeightBehavior}
172 final ui.TextHeightBehavior? textHeightBehavior;
173
174 /// The closest instance of this class that encloses the given context.
175 ///
176 /// If no such instance exists, returns an instance created by
177 /// [DefaultTextStyle.fallback], which contains fallback values.
178 ///
179 /// Typical usage is as follows:
180 ///
181 /// ```dart
182 /// DefaultTextStyle style = DefaultTextStyle.of(context);
183 /// ```
184 static DefaultTextStyle of(BuildContext context) {
185 return context.dependOnInheritedWidgetOfExactType<DefaultTextStyle>() ??
186 const DefaultTextStyle.fallback();
187 }
188
189 @override
190 bool updateShouldNotify(DefaultTextStyle oldWidget) {
191 return style != oldWidget.style ||
192 textAlign != oldWidget.textAlign ||
193 softWrap != oldWidget.softWrap ||
194 overflow != oldWidget.overflow ||
195 maxLines != oldWidget.maxLines ||
196 textWidthBasis != oldWidget.textWidthBasis ||
197 textHeightBehavior != oldWidget.textHeightBehavior;
198 }
199
200 @override
201 Widget wrap(BuildContext context, Widget child) {
202 return DefaultTextStyle(
203 style: style,
204 textAlign: textAlign,
205 softWrap: softWrap,
206 overflow: overflow,
207 maxLines: maxLines,
208 textWidthBasis: textWidthBasis,
209 textHeightBehavior: textHeightBehavior,
210 child: child,
211 );
212 }
213
214 @override
215 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
216 super.debugFillProperties(properties);
217 style.debugFillProperties(properties);
218 properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
219 properties.add(
220 FlagProperty(
221 'softWrap',
222 value: softWrap,
223 ifTrue: 'wrapping at box width',
224 ifFalse: 'no wrapping except at line break characters',
225 showName: true,
226 ),
227 );
228 properties.add(EnumProperty<TextOverflow>('overflow', overflow, defaultValue: null));
229 properties.add(IntProperty('maxLines', maxLines, defaultValue: null));
230 properties.add(
231 EnumProperty<TextWidthBasis>(
232 'textWidthBasis',
233 textWidthBasis,
234 defaultValue: TextWidthBasis.parent,
235 ),
236 );
237 properties.add(
238 DiagnosticsProperty<ui.TextHeightBehavior>(
239 'textHeightBehavior',
240 textHeightBehavior,
241 defaultValue: null,
242 ),
243 );
244 }
245}
246
247class _NullWidget extends StatelessWidget {
248 const _NullWidget();
249
250 @override
251 Widget build(BuildContext context) {
252 throw FlutterError(
253 'A DefaultTextStyle constructed with DefaultTextStyle.fallback cannot be incorporated into the widget tree, '
254 'it is meant only to provide a fallback value returned by DefaultTextStyle.of() '
255 'when no enclosing default text style is present in a BuildContext.',
256 );
257 }
258}
259
260/// The [TextHeightBehavior] that will apply to descendant [Text] and [EditableText]
261/// widgets which have not explicitly set [Text.textHeightBehavior].
262///
263/// If there is a [DefaultTextStyle] with a non-null [DefaultTextStyle.textHeightBehavior]
264/// below this widget, the [DefaultTextStyle.textHeightBehavior] will be used
265/// over this widget's [TextHeightBehavior].
266///
267/// See also:
268///
269/// * [DefaultTextStyle], which defines a [TextStyle] to apply to descendant
270/// [Text] widgets.
271class DefaultTextHeightBehavior extends InheritedTheme {
272 /// Creates a default text height behavior for the given subtree.
273 const DefaultTextHeightBehavior({
274 super.key,
275 required this.textHeightBehavior,
276 required super.child,
277 });
278
279 /// {@macro dart.ui.textHeightBehavior}
280 final TextHeightBehavior textHeightBehavior;
281
282 /// The closest instance of [DefaultTextHeightBehavior] that encloses the
283 /// given context, or null if none is found.
284 ///
285 /// If no such instance exists, this method will return `null`.
286 ///
287 /// Calling this method will create a dependency on the closest
288 /// [DefaultTextHeightBehavior] in the [context], if there is one.
289 ///
290 /// Typical usage is as follows:
291 ///
292 /// ```dart
293 /// TextHeightBehavior? defaultTextHeightBehavior = DefaultTextHeightBehavior.of(context);
294 /// ```
295 ///
296 /// See also:
297 ///
298 /// * [DefaultTextHeightBehavior.maybeOf], which is similar to this method,
299 /// but asserts if no [DefaultTextHeightBehavior] ancestor is found.
300 static TextHeightBehavior? maybeOf(BuildContext context) {
301 return context
302 .dependOnInheritedWidgetOfExactType<DefaultTextHeightBehavior>()
303 ?.textHeightBehavior;
304 }
305
306 /// The closest instance of [DefaultTextHeightBehavior] that encloses the
307 /// given context.
308 ///
309 /// If no such instance exists, this method will assert in debug mode, and
310 /// throw an exception in release mode.
311 ///
312 /// Typical usage is as follows:
313 ///
314 /// ```dart
315 /// TextHeightBehavior defaultTextHeightBehavior = DefaultTextHeightBehavior.of(context);
316 /// ```
317 ///
318 /// Calling this method will create a dependency on the closest
319 /// [DefaultTextHeightBehavior] in the [context].
320 ///
321 /// See also:
322 ///
323 /// * [DefaultTextHeightBehavior.maybeOf], which is similar to this method,
324 /// but returns null if no [DefaultTextHeightBehavior] ancestor is found.
325 static TextHeightBehavior of(BuildContext context) {
326 final TextHeightBehavior? behavior = maybeOf(context);
327 assert(() {
328 if (behavior == null) {
329 throw FlutterError(
330 'DefaultTextHeightBehavior.of() was called with a context that does not contain a '
331 'DefaultTextHeightBehavior widget.\n'
332 'No DefaultTextHeightBehavior widget ancestor could be found starting from the '
333 'context that was passed to DefaultTextHeightBehavior.of(). This can happen '
334 'because you are using a widget that looks for a DefaultTextHeightBehavior '
335 'ancestor, but no such ancestor exists.\n'
336 'The context used was:\n'
337 ' $context',
338 );
339 }
340 return true;
341 }());
342 return behavior!;
343 }
344
345 @override
346 bool updateShouldNotify(DefaultTextHeightBehavior oldWidget) {
347 return textHeightBehavior != oldWidget.textHeightBehavior;
348 }
349
350 @override
351 Widget wrap(BuildContext context, Widget child) {
352 return DefaultTextHeightBehavior(textHeightBehavior: textHeightBehavior, child: child);
353 }
354
355 @override
356 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
357 super.debugFillProperties(properties);
358 properties.add(
359 DiagnosticsProperty<ui.TextHeightBehavior>(
360 'textHeightBehavior',
361 textHeightBehavior,
362 defaultValue: null,
363 ),
364 );
365 }
366}
367
368/// A run of text with a single style.
369///
370/// The [Text] widget displays a string of text with single style. The string
371/// might break across multiple lines or might all be displayed on the same line
372/// depending on the layout constraints.
373///
374/// The [style] argument is optional. When omitted, the text will use the style
375/// from the closest enclosing [DefaultTextStyle]. If the given style's
376/// [TextStyle.inherit] property is true (the default), the given style will
377/// be merged with the closest enclosing [DefaultTextStyle]. This merging
378/// behavior is useful, for example, to make the text bold while using the
379/// default font family and size.
380///
381/// {@tool snippet}
382///
383/// This example shows how to display text using the [Text] widget with the
384/// [overflow] set to [TextOverflow.ellipsis].
385///
386/// ![If the text overflows, the Text widget displays an ellipsis to trim the overflowing text](https://flutter.github.io/assets-for-api-docs/assets/widgets/text_ellipsis.png)
387///
388/// ```dart
389/// Container(
390/// width: 100,
391/// decoration: BoxDecoration(border: Border.all()),
392/// child: Text(overflow: TextOverflow.ellipsis, 'Hello $_name, how are you?'))
393/// ```
394/// {@end-tool}
395///
396/// {@tool snippet}
397///
398/// Setting [maxLines] to `1` is not equivalent to disabling soft wrapping with
399/// [softWrap]. This is apparent when using [TextOverflow.fade] as the following
400/// examples show.
401///
402/// ![If a second line overflows the Text widget displays a horizontal fade](https://flutter.github.io/assets-for-api-docs/assets/widgets/text_fade_max_lines.png)
403///
404/// ```dart
405/// Text(
406/// overflow: TextOverflow.fade,
407/// maxLines: 1,
408/// 'Hello $_name, how are you?')
409/// ```
410///
411/// Here soft wrapping is enabled and the [Text] widget tries to wrap the words
412/// "how are you?" to a second line. This is prevented by the [maxLines] value
413/// of `1`. The result is that a second line overflows and the fade appears in a
414/// horizontal direction at the bottom.
415///
416/// ![If a single line overflows the Text widget displays a horizontal fade](https://flutter.github.io/assets-for-api-docs/assets/widgets/text_fade_soft_wrap.png)
417///
418/// ```dart
419/// Text(
420/// overflow: TextOverflow.fade,
421/// softWrap: false,
422/// 'Hello $_name, how are you?')
423/// ```
424///
425/// Here soft wrapping is disabled with `softWrap: false` and the [Text] widget
426/// attempts to display its text in a single unbroken line. The result is that
427/// the single line overflows and the fade appears in a vertical direction at
428/// the right.
429///
430/// {@end-tool}
431///
432/// Using the [Text.rich] constructor, the [Text] widget can
433/// display a paragraph with differently styled [TextSpan]s. The sample
434/// that follows displays "Hello beautiful world" with different styles
435/// for each word.
436///
437/// {@tool snippet}
438///
439/// ![The word "Hello" is shown with the default text styles. The word "beautiful" is italicized. The word "world" is bold.](https://flutter.github.io/assets-for-api-docs/assets/widgets/text_rich.png)
440///
441/// ```dart
442/// const Text.rich(
443/// TextSpan(
444/// text: 'Hello', // default text style
445/// children: <TextSpan>[
446/// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)),
447/// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)),
448/// ],
449/// ),
450/// )
451/// ```
452/// {@end-tool}
453///
454/// ## Interactivity
455///
456/// To make [Text] react to touch events, wrap it in a [GestureDetector] widget
457/// with a [GestureDetector.onTap] handler.
458///
459/// In a Material Design application, consider using a [TextButton] instead, or
460/// if that isn't appropriate, at least using an [InkWell] instead of
461/// [GestureDetector].
462///
463/// To make sections of the text interactive, use [RichText] and specify a
464/// [TapGestureRecognizer] as the [TextSpan.recognizer] of the relevant part of
465/// the text.
466///
467/// ## Selection
468///
469/// [Text] is not selectable by default. To make a [Text] selectable, one can
470/// wrap a subtree with a [SelectionArea] widget. To exclude a part of a subtree
471/// under [SelectionArea] from selection, once can also wrap that part of the
472/// subtree with [SelectionContainer.disabled].
473///
474/// {@tool dartpad}
475/// This sample demonstrates how to disable selection for a Text under a
476/// SelectionArea.
477///
478/// ** See code in examples/api/lib/material/selection_container/selection_container_disabled.0.dart **
479/// {@end-tool}
480///
481/// See also:
482///
483/// * [RichText], which gives you more control over the text styles.
484/// * [DefaultTextStyle], which sets default styles for [Text] widgets.
485/// * [SelectableRegion], which provides an overview of the selection system.
486class Text extends StatelessWidget {
487 /// Creates a text widget.
488 ///
489 /// If the [style] argument is null, the text will use the style from the
490 /// closest enclosing [DefaultTextStyle].
491 ///
492 /// The [overflow] property's behavior is affected by the [softWrap] argument.
493 /// If the [softWrap] is true or null, the glyph causing overflow, and those
494 /// that follow, will not be rendered. Otherwise, it will be shown with the
495 /// given overflow option.
496 const Text(
497 String this.data, {
498 super.key,
499 this.style,
500 this.strutStyle,
501 this.textAlign,
502 this.textDirection,
503 this.locale,
504 this.softWrap,
505 this.overflow,
506 @Deprecated(
507 'Use textScaler instead. '
508 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
509 'This feature was deprecated after v3.12.0-2.0.pre.',
510 )
511 this.textScaleFactor,
512 this.textScaler,
513 this.maxLines,
514 this.semanticsLabel,
515 this.semanticsIdentifier,
516 this.textWidthBasis,
517 this.textHeightBehavior,
518 this.selectionColor,
519 }) : textSpan = null,
520 assert(
521 textScaler == null || textScaleFactor == null,
522 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
523 );
524
525 /// Creates a text widget with a [InlineSpan].
526 ///
527 /// The following subclasses of [InlineSpan] may be used to build rich text:
528 ///
529 /// * [TextSpan]s define text and children [InlineSpan]s.
530 /// * [WidgetSpan]s define embedded inline widgets.
531 ///
532 /// See [RichText] which provides a lower-level way to draw text.
533 const Text.rich(
534 InlineSpan this.textSpan, {
535 super.key,
536 this.style,
537 this.strutStyle,
538 this.textAlign,
539 this.textDirection,
540 this.locale,
541 this.softWrap,
542 this.overflow,
543 @Deprecated(
544 'Use textScaler instead. '
545 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
546 'This feature was deprecated after v3.12.0-2.0.pre.',
547 )
548 this.textScaleFactor,
549 this.textScaler,
550 this.maxLines,
551 this.semanticsLabel,
552 this.semanticsIdentifier,
553 this.textWidthBasis,
554 this.textHeightBehavior,
555 this.selectionColor,
556 }) : data = null,
557 assert(
558 textScaler == null || textScaleFactor == null,
559 'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
560 );
561
562 /// The text to display.
563 ///
564 /// This will be null if a [textSpan] is provided instead.
565 final String? data;
566
567 /// The text to display as a [InlineSpan].
568 ///
569 /// This will be null if [data] is provided instead.
570 final InlineSpan? textSpan;
571
572 /// If non-null, the style to use for this text.
573 ///
574 /// If the style's "inherit" property is true, the style will be merged with
575 /// the closest enclosing [DefaultTextStyle]. Otherwise, the style will
576 /// replace the closest enclosing [DefaultTextStyle].
577 final TextStyle? style;
578
579 /// {@macro flutter.painting.textPainter.strutStyle}
580 final StrutStyle? strutStyle;
581
582 /// How the text should be aligned horizontally.
583 final TextAlign? textAlign;
584
585 /// The directionality of the text.
586 ///
587 /// This decides how [textAlign] values like [TextAlign.start] and
588 /// [TextAlign.end] are interpreted.
589 ///
590 /// This is also used to disambiguate how to render bidirectional text. For
591 /// example, if the [data] is an English phrase followed by a Hebrew phrase,
592 /// in a [TextDirection.ltr] context the English phrase will be on the left
593 /// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
594 /// context, the English phrase will be on the right and the Hebrew phrase on
595 /// its left.
596 ///
597 /// Defaults to the ambient [Directionality], if any.
598 final TextDirection? textDirection;
599
600 /// Used to select a font when the same Unicode character can
601 /// be rendered differently, depending on the locale.
602 ///
603 /// It's rarely necessary to set this property. By default its value
604 /// is inherited from the enclosing app with `Localizations.localeOf(context)`.
605 ///
606 /// See [RenderParagraph.locale] for more information.
607 final Locale? locale;
608
609 /// Whether the text should break at soft line breaks.
610 ///
611 /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
612 final bool? softWrap;
613
614 /// How visual overflow should be handled.
615 ///
616 /// If this is null [TextStyle.overflow] will be used, otherwise the value
617 /// from the nearest [DefaultTextStyle] ancestor will be used.
618 final TextOverflow? overflow;
619
620 /// Deprecated. Will be removed in a future version of Flutter. Use
621 /// [textScaler] instead.
622 ///
623 /// The number of font pixels for each logical pixel.
624 ///
625 /// For example, if the text scale factor is 1.5, text will be 50% larger than
626 /// the specified font size.
627 ///
628 /// The value given to the constructor as textScaleFactor. If null, will
629 /// use the [MediaQueryData.textScaleFactor] obtained from the ambient
630 /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
631 @Deprecated(
632 'Use textScaler instead. '
633 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
634 'This feature was deprecated after v3.12.0-2.0.pre.',
635 )
636 final double? textScaleFactor;
637
638 /// {@macro flutter.painting.textPainter.textScaler}
639 final TextScaler? textScaler;
640
641 /// An optional maximum number of lines for the text to span, wrapping if necessary.
642 /// If the text exceeds the given number of lines, it will be truncated according
643 /// to [overflow].
644 ///
645 /// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
646 /// edge of the box.
647 ///
648 /// If this is null, but there is an ambient [DefaultTextStyle] that specifies
649 /// an explicit number for its [DefaultTextStyle.maxLines], then the
650 /// [DefaultTextStyle] value will take precedence. You can use a [RichText]
651 /// widget directly to entirely override the [DefaultTextStyle].
652 final int? maxLines;
653
654 /// {@template flutter.widgets.Text.semanticsLabel}
655 /// An alternative semantics label for this text.
656 ///
657 /// If present, the semantics of this widget will contain this value instead
658 /// of the actual text. This will overwrite any of the semantics labels applied
659 /// directly to the [TextSpan]s.
660 ///
661 /// This is useful for replacing abbreviations or shorthands with the full
662 /// text value:
663 ///
664 /// ```dart
665 /// const Text(r'$$', semanticsLabel: 'Double dollars')
666 /// ```
667 /// {@endtemplate}
668 final String? semanticsLabel;
669
670 /// A unique identifier for the semantics node for this widget.
671 ///
672 /// This is useful for cases where the text widget needs to have a uniquely
673 /// identifiable ID that is recognized through the automation tools without
674 /// having a dependency on the actual content of the text that can possibly be
675 /// dynamic in nature.
676 final String? semanticsIdentifier;
677
678 /// {@macro flutter.painting.textPainter.textWidthBasis}
679 final TextWidthBasis? textWidthBasis;
680
681 /// {@macro dart.ui.textHeightBehavior}
682 final ui.TextHeightBehavior? textHeightBehavior;
683
684 /// The color to use when painting the selection.
685 ///
686 /// This is ignored if [SelectionContainer.maybeOf] returns null
687 /// in the [BuildContext] of the [Text] widget.
688 ///
689 /// If null, the ambient [DefaultSelectionStyle] is used (if any); failing
690 /// that, the selection color defaults to [DefaultSelectionStyle.defaultColor]
691 /// (semi-transparent grey).
692 final Color? selectionColor;
693
694 @override
695 Widget build(BuildContext context) {
696 final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
697 TextStyle? effectiveTextStyle = style;
698 if (style == null || style!.inherit) {
699 effectiveTextStyle = defaultTextStyle.style.merge(style);
700 }
701 if (MediaQuery.boldTextOf(context)) {
702 effectiveTextStyle = effectiveTextStyle!.merge(const TextStyle(fontWeight: FontWeight.bold));
703 }
704 final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
705 final TextScaler textScaler = switch ((this.textScaler, textScaleFactor)) {
706 (final TextScaler textScaler, _) => textScaler,
707 // For unmigrated apps, fall back to textScaleFactor.
708 (null, final double textScaleFactor) => TextScaler.linear(textScaleFactor),
709 (null, null) => MediaQuery.textScalerOf(context),
710 };
711 late Widget result;
712 if (registrar != null) {
713 result = MouseRegion(
714 cursor: DefaultSelectionStyle.of(context).mouseCursor ?? SystemMouseCursors.text,
715 child: _SelectableTextContainer(
716 textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
717 textDirection:
718 textDirection, // RichText uses Directionality.of to obtain a default if this is null.
719 locale:
720 locale, // RichText uses Localizations.localeOf to obtain a default if this is null
721 softWrap: softWrap ?? defaultTextStyle.softWrap,
722 overflow: overflow ?? effectiveTextStyle?.overflow ?? defaultTextStyle.overflow,
723 textScaler: textScaler,
724 maxLines: maxLines ?? defaultTextStyle.maxLines,
725 strutStyle: strutStyle,
726 textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
727 textHeightBehavior:
728 textHeightBehavior ??
729 defaultTextStyle.textHeightBehavior ??
730 DefaultTextHeightBehavior.maybeOf(context),
731 selectionColor:
732 selectionColor ??
733 DefaultSelectionStyle.of(context).selectionColor ??
734 DefaultSelectionStyle.defaultColor,
735 text: TextSpan(
736 style: effectiveTextStyle,
737 text: data,
738 locale: locale,
739 children: textSpan != null ? <InlineSpan>[textSpan!] : null,
740 ),
741 ),
742 );
743 } else {
744 result = RichText(
745 textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
746 textDirection:
747 textDirection, // RichText uses Directionality.of to obtain a default if this is null.
748 locale: locale, // RichText uses Localizations.localeOf to obtain a default if this is null
749 softWrap: softWrap ?? defaultTextStyle.softWrap,
750 overflow: overflow ?? effectiveTextStyle?.overflow ?? defaultTextStyle.overflow,
751 textScaler: textScaler,
752 maxLines: maxLines ?? defaultTextStyle.maxLines,
753 strutStyle: strutStyle,
754 textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
755 textHeightBehavior:
756 textHeightBehavior ??
757 defaultTextStyle.textHeightBehavior ??
758 DefaultTextHeightBehavior.maybeOf(context),
759 selectionColor:
760 selectionColor ??
761 DefaultSelectionStyle.of(context).selectionColor ??
762 DefaultSelectionStyle.defaultColor,
763 text: TextSpan(
764 style: effectiveTextStyle,
765 text: data,
766 locale: locale,
767 children: textSpan != null ? <InlineSpan>[textSpan!] : null,
768 ),
769 );
770 }
771 if (semanticsLabel != null || semanticsIdentifier != null) {
772 result = Semantics(
773 textDirection: textDirection,
774 label: semanticsLabel,
775 identifier: semanticsIdentifier,
776 child: ExcludeSemantics(excluding: semanticsLabel != null, child: result),
777 );
778 }
779 return result;
780 }
781
782 @override
783 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
784 super.debugFillProperties(properties);
785 properties.add(StringProperty('data', data, showName: false));
786 if (textSpan != null) {
787 properties.add(
788 textSpan!.toDiagnosticsNode(name: 'textSpan', style: DiagnosticsTreeStyle.transition),
789 );
790 }
791 style?.debugFillProperties(properties);
792 properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
793 properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
794 properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
795 properties.add(
796 FlagProperty(
797 'softWrap',
798 value: softWrap,
799 ifTrue: 'wrapping at box width',
800 ifFalse: 'no wrapping except at line break characters',
801 showName: true,
802 ),
803 );
804 properties.add(EnumProperty<TextOverflow>('overflow', overflow, defaultValue: null));
805 properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
806 properties.add(IntProperty('maxLines', maxLines, defaultValue: null));
807 properties.add(
808 EnumProperty<TextWidthBasis>('textWidthBasis', textWidthBasis, defaultValue: null),
809 );
810 properties.add(
811 DiagnosticsProperty<ui.TextHeightBehavior>(
812 'textHeightBehavior',
813 textHeightBehavior,
814 defaultValue: null,
815 ),
816 );
817 if (semanticsLabel != null) {
818 properties.add(StringProperty('semanticsLabel', semanticsLabel));
819 }
820 if (semanticsIdentifier != null) {
821 properties.add(StringProperty('semanticsIdentifier', semanticsIdentifier));
822 }
823 }
824}
825
826class _SelectableTextContainer extends StatefulWidget {
827 const _SelectableTextContainer({
828 required this.text,
829 required this.textAlign,
830 this.textDirection,
831 required this.softWrap,
832 required this.overflow,
833 required this.textScaler,
834 this.maxLines,
835 this.locale,
836 this.strutStyle,
837 required this.textWidthBasis,
838 this.textHeightBehavior,
839 required this.selectionColor,
840 });
841
842 final TextSpan text;
843 final TextAlign textAlign;
844 final TextDirection? textDirection;
845 final bool softWrap;
846 final TextOverflow overflow;
847 final TextScaler textScaler;
848 final int? maxLines;
849 final Locale? locale;
850 final StrutStyle? strutStyle;
851 final TextWidthBasis textWidthBasis;
852 final ui.TextHeightBehavior? textHeightBehavior;
853 final Color selectionColor;
854
855 @override
856 State<_SelectableTextContainer> createState() => _SelectableTextContainerState();
857}
858
859class _SelectableTextContainerState extends State<_SelectableTextContainer> {
860 late final _SelectableTextContainerDelegate _selectionDelegate;
861 final GlobalKey _textKey = GlobalKey();
862
863 @override
864 void initState() {
865 super.initState();
866 _selectionDelegate = _SelectableTextContainerDelegate(_textKey);
867 }
868
869 @override
870 void dispose() {
871 _selectionDelegate.dispose();
872 super.dispose();
873 }
874
875 @override
876 Widget build(BuildContext context) {
877 return SelectionContainer(
878 delegate: _selectionDelegate,
879 // Use [_RichText] wrapper so the underlying [RenderParagraph] can register
880 // its [Selectable]s to the [SelectionContainer] created by this widget.
881 child: _RichText(
882 textKey: _textKey,
883 textAlign: widget.textAlign,
884 textDirection: widget.textDirection,
885 locale: widget.locale,
886 softWrap: widget.softWrap,
887 overflow: widget.overflow,
888 textScaler: widget.textScaler,
889 maxLines: widget.maxLines,
890 strutStyle: widget.strutStyle,
891 textWidthBasis: widget.textWidthBasis,
892 textHeightBehavior: widget.textHeightBehavior,
893 selectionColor: widget.selectionColor,
894 text: widget.text,
895 ),
896 );
897 }
898}
899
900class _RichText extends StatelessWidget {
901 const _RichText({
902 this.textKey,
903 required this.text,
904 required this.textAlign,
905 this.textDirection,
906 required this.softWrap,
907 required this.overflow,
908 required this.textScaler,
909 this.maxLines,
910 this.locale,
911 this.strutStyle,
912 required this.textWidthBasis,
913 this.textHeightBehavior,
914 required this.selectionColor,
915 });
916
917 final GlobalKey? textKey;
918 final InlineSpan text;
919 final TextAlign textAlign;
920 final TextDirection? textDirection;
921 final bool softWrap;
922 final TextOverflow overflow;
923 final TextScaler textScaler;
924 final int? maxLines;
925 final Locale? locale;
926 final StrutStyle? strutStyle;
927 final TextWidthBasis textWidthBasis;
928 final ui.TextHeightBehavior? textHeightBehavior;
929 final Color selectionColor;
930
931 @override
932 Widget build(BuildContext context) {
933 final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
934 return RichText(
935 key: textKey,
936 textAlign: textAlign,
937 textDirection: textDirection,
938 locale: locale,
939 softWrap: softWrap,
940 overflow: overflow,
941 textScaler: textScaler,
942 maxLines: maxLines,
943 strutStyle: strutStyle,
944 textWidthBasis: textWidthBasis,
945 textHeightBehavior: textHeightBehavior,
946 selectionRegistrar: registrar,
947 selectionColor: selectionColor,
948 text: text,
949 );
950 }
951}
952
953// In practice some selectables like widgetspan shift several pixels. So when
954// the vertical position diff is within the threshold, compare the horizontal
955// position to make the compareScreenOrder function more robust.
956const double _kSelectableVerticalComparingThreshold = 3.0;
957
958class _SelectableTextContainerDelegate extends StaticSelectionContainerDelegate {
959 _SelectableTextContainerDelegate(GlobalKey textKey) : _textKey = textKey;
960
961 final GlobalKey _textKey;
962 RenderParagraph get paragraph => _textKey.currentContext!.findRenderObject()! as RenderParagraph;
963
964 @override
965 SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) {
966 final SelectionResult result = _handleSelectParagraph(event);
967 super.didReceiveSelectionBoundaryEvents();
968 return result;
969 }
970
971 SelectionResult _handleSelectParagraph(SelectParagraphSelectionEvent event) {
972 if (event.absorb) {
973 for (int index = 0; index < selectables.length; index += 1) {
974 dispatchSelectionEventToChild(selectables[index], event);
975 }
976 currentSelectionStartIndex = 0;
977 currentSelectionEndIndex = selectables.length - 1;
978 return SelectionResult.next;
979 }
980
981 // First pass, if the position is on a placeholder then dispatch the selection
982 // event to the [Selectable] at the location and terminate.
983 for (int index = 0; index < selectables.length; index += 1) {
984 final bool selectableIsPlaceholder = !paragraph.selectableBelongsToParagraph(
985 selectables[index],
986 );
987 if (selectableIsPlaceholder && selectables[index].boundingBoxes.isNotEmpty) {
988 for (final Rect rect in selectables[index].boundingBoxes) {
989 final Rect globalRect = MatrixUtils.transformRect(
990 selectables[index].getTransformTo(null),
991 rect,
992 );
993 if (globalRect.contains(event.globalPosition)) {
994 currentSelectionStartIndex = currentSelectionEndIndex = index;
995 return dispatchSelectionEventToChild(selectables[index], event);
996 }
997 }
998 }
999 }
1000
1001 SelectionResult? lastSelectionResult;
1002 bool foundStart = false;
1003 int? lastNextIndex;
1004 for (int index = 0; index < selectables.length; index += 1) {
1005 if (!paragraph.selectableBelongsToParagraph(selectables[index])) {
1006 if (foundStart) {
1007 final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent(
1008 globalPosition: event.globalPosition,
1009 absorb: true,
1010 );
1011 final SelectionResult result = dispatchSelectionEventToChild(
1012 selectables[index],
1013 synthesizedEvent,
1014 );
1015 if (selectables.length - 1 == index) {
1016 currentSelectionEndIndex = index;
1017 _flushInactiveSelections();
1018 return result;
1019 }
1020 }
1021 continue;
1022 }
1023 final SelectionGeometry existingGeometry = selectables[index].value;
1024 lastSelectionResult = dispatchSelectionEventToChild(selectables[index], event);
1025 if (index == selectables.length - 1 && lastSelectionResult == SelectionResult.next) {
1026 if (foundStart) {
1027 currentSelectionEndIndex = index;
1028 } else {
1029 currentSelectionStartIndex = currentSelectionEndIndex = index;
1030 }
1031 return SelectionResult.next;
1032 }
1033 if (lastSelectionResult == SelectionResult.next) {
1034 if (selectables[index].value == existingGeometry && !foundStart) {
1035 lastNextIndex = index;
1036 }
1037 if (selectables[index].value != existingGeometry && !foundStart) {
1038 assert(selectables[index].boundingBoxes.isNotEmpty);
1039 assert(selectables[index].value.selectionRects.isNotEmpty);
1040 final bool selectionAtStartOfSelectable = selectables[index].boundingBoxes[0].overlaps(
1041 selectables[index].value.selectionRects[0],
1042 );
1043 int startIndex = 0;
1044 if (lastNextIndex != null && selectionAtStartOfSelectable) {
1045 startIndex = lastNextIndex + 1;
1046 } else {
1047 startIndex = lastNextIndex == null && selectionAtStartOfSelectable ? 0 : index;
1048 }
1049 for (int i = startIndex; i < index; i += 1) {
1050 final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent(
1051 globalPosition: event.globalPosition,
1052 absorb: true,
1053 );
1054 dispatchSelectionEventToChild(selectables[i], synthesizedEvent);
1055 }
1056 currentSelectionStartIndex = startIndex;
1057 foundStart = true;
1058 }
1059 continue;
1060 }
1061 if (index == 0 && lastSelectionResult == SelectionResult.previous) {
1062 return SelectionResult.previous;
1063 }
1064 if (selectables[index].value != existingGeometry) {
1065 if (!foundStart && lastNextIndex == null) {
1066 currentSelectionStartIndex = 0;
1067 for (int i = 0; i < index; i += 1) {
1068 final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent(
1069 globalPosition: event.globalPosition,
1070 absorb: true,
1071 );
1072 dispatchSelectionEventToChild(selectables[i], synthesizedEvent);
1073 }
1074 }
1075 currentSelectionEndIndex = index;
1076 // Geometry has changed as a result of select paragraph, need to clear the
1077 // selection of other selectables to keep selection in sync.
1078 _flushInactiveSelections();
1079 }
1080 return SelectionResult.end;
1081 }
1082 assert(lastSelectionResult == null);
1083 return SelectionResult.end;
1084 }
1085
1086 /// Initializes the selection of the selectable children.
1087 ///
1088 /// The goal is to find the selectable child that contains the selection edge.
1089 /// Returns [SelectionResult.end] if the selection edge ends on any of the
1090 /// children. Otherwise, it returns [SelectionResult.previous] if the selection
1091 /// does not reach any of its children. Returns [SelectionResult.next]
1092 /// if the selection reaches the end of its children.
1093 ///
1094 /// Ideally, this method should only be called twice at the beginning of the
1095 /// drag selection, once for start edge update event, once for end edge update
1096 /// event.
1097 SelectionResult _initSelection(SelectionEdgeUpdateEvent event, {required bool isEnd}) {
1098 assert(
1099 (isEnd && currentSelectionEndIndex == -1) || (!isEnd && currentSelectionStartIndex == -1),
1100 );
1101 SelectionResult? finalResult;
1102 // Begin the search for the selection edge at the opposite edge if it exists.
1103 final bool hasOppositeEdge = isEnd
1104 ? currentSelectionStartIndex != -1
1105 : currentSelectionEndIndex != -1;
1106 int newIndex = switch ((isEnd, hasOppositeEdge)) {
1107 (true, true) => currentSelectionStartIndex,
1108 (true, false) => 0,
1109 (false, true) => currentSelectionEndIndex,
1110 (false, false) => 0,
1111 };
1112 bool? forward;
1113 late SelectionResult currentSelectableResult;
1114 // This loop sends the selection event to one of the following to determine
1115 // the direction of the search.
1116 // - The opposite edge index if it exists.
1117 // - Index 0 if the opposite edge index does not exist.
1118 //
1119 // If the result is `SelectionResult.next`, this loop look backward.
1120 // Otherwise, it looks forward.
1121 //
1122 // The terminate condition are:
1123 // 1. the selectable returns end, pending, none.
1124 // 2. the selectable returns previous when looking forward.
1125 // 2. the selectable returns next when looking backward.
1126 while (newIndex < selectables.length && newIndex >= 0 && finalResult == null) {
1127 currentSelectableResult = dispatchSelectionEventToChild(selectables[newIndex], event);
1128 switch (currentSelectableResult) {
1129 case SelectionResult.end:
1130 case SelectionResult.pending:
1131 case SelectionResult.none:
1132 finalResult = currentSelectableResult;
1133 case SelectionResult.next:
1134 if (forward == false) {
1135 newIndex += 1;
1136 finalResult = SelectionResult.end;
1137 } else if (newIndex == selectables.length - 1) {
1138 finalResult = currentSelectableResult;
1139 } else {
1140 forward = true;
1141 newIndex += 1;
1142 }
1143 case SelectionResult.previous:
1144 if (forward ?? false) {
1145 newIndex -= 1;
1146 finalResult = SelectionResult.end;
1147 } else if (newIndex == 0) {
1148 finalResult = currentSelectableResult;
1149 } else {
1150 forward = false;
1151 newIndex -= 1;
1152 }
1153 }
1154 }
1155 if (isEnd) {
1156 currentSelectionEndIndex = newIndex;
1157 } else {
1158 currentSelectionStartIndex = newIndex;
1159 }
1160 _flushInactiveSelections();
1161 return finalResult!;
1162 }
1163
1164 SelectionResult _adjustSelection(SelectionEdgeUpdateEvent event, {required bool isEnd}) {
1165 assert(() {
1166 if (isEnd) {
1167 assert(currentSelectionEndIndex < selectables.length && currentSelectionEndIndex >= 0);
1168 return true;
1169 }
1170 assert(currentSelectionStartIndex < selectables.length && currentSelectionStartIndex >= 0);
1171 return true;
1172 }());
1173 SelectionResult? finalResult;
1174 // Determines if the edge being adjusted is within the current viewport.
1175 // - If so, we begin the search for the new selection edge position at the
1176 // currentSelectionEndIndex/currentSelectionStartIndex.
1177 // - If not, we attempt to locate the new selection edge starting from
1178 // the opposite end.
1179 // - If neither edge is in the current viewport, the search for the new
1180 // selection edge position begins at 0.
1181 //
1182 // This can happen when there is a scrollable child and the edge being adjusted
1183 // has been scrolled out of view.
1184 final bool isCurrentEdgeWithinViewport = isEnd
1185 ? value.endSelectionPoint != null
1186 : value.startSelectionPoint != null;
1187 final bool isOppositeEdgeWithinViewport = isEnd
1188 ? value.startSelectionPoint != null
1189 : value.endSelectionPoint != null;
1190 int newIndex = switch ((isEnd, isCurrentEdgeWithinViewport, isOppositeEdgeWithinViewport)) {
1191 (true, true, true) => currentSelectionEndIndex,
1192 (true, true, false) => currentSelectionEndIndex,
1193 (true, false, true) => currentSelectionStartIndex,
1194 (true, false, false) => 0,
1195 (false, true, true) => currentSelectionStartIndex,
1196 (false, true, false) => currentSelectionStartIndex,
1197 (false, false, true) => currentSelectionEndIndex,
1198 (false, false, false) => 0,
1199 };
1200 bool? forward;
1201 late SelectionResult currentSelectableResult;
1202 // This loop sends the selection event to one of the following to determine
1203 // the direction of the search.
1204 // - currentSelectionEndIndex/currentSelectionStartIndex if the current edge
1205 // is in the current viewport.
1206 // - The opposite edge index if the current edge is not in the current viewport.
1207 // - Index 0 if neither edge is in the current viewport.
1208 //
1209 // If the result is `SelectionResult.next`, this loop look backward.
1210 // Otherwise, it looks forward.
1211 //
1212 // The terminate condition are:
1213 // 1. the selectable returns end, pending, none.
1214 // 2. the selectable returns previous when looking forward.
1215 // 2. the selectable returns next when looking backward.
1216 while (newIndex < selectables.length && newIndex >= 0 && finalResult == null) {
1217 currentSelectableResult = dispatchSelectionEventToChild(selectables[newIndex], event);
1218 switch (currentSelectableResult) {
1219 case SelectionResult.end:
1220 case SelectionResult.pending:
1221 case SelectionResult.none:
1222 finalResult = currentSelectableResult;
1223 case SelectionResult.next:
1224 if (forward == false) {
1225 newIndex += 1;
1226 finalResult = SelectionResult.end;
1227 } else if (newIndex == selectables.length - 1) {
1228 finalResult = currentSelectableResult;
1229 } else {
1230 forward = true;
1231 newIndex += 1;
1232 }
1233 case SelectionResult.previous:
1234 if (forward ?? false) {
1235 newIndex -= 1;
1236 finalResult = SelectionResult.end;
1237 } else if (newIndex == 0) {
1238 finalResult = currentSelectableResult;
1239 } else {
1240 forward = false;
1241 newIndex -= 1;
1242 }
1243 }
1244 }
1245 if (isEnd) {
1246 final bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex;
1247 if (forward != null &&
1248 ((!forwardSelection && forward && newIndex >= currentSelectionStartIndex) ||
1249 (forwardSelection && !forward && newIndex <= currentSelectionStartIndex))) {
1250 currentSelectionStartIndex = currentSelectionEndIndex;
1251 }
1252 currentSelectionEndIndex = newIndex;
1253 } else {
1254 final bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex;
1255 if (forward != null &&
1256 ((!forwardSelection && !forward && newIndex <= currentSelectionEndIndex) ||
1257 (forwardSelection && forward && newIndex >= currentSelectionEndIndex))) {
1258 currentSelectionEndIndex = currentSelectionStartIndex;
1259 }
1260 currentSelectionStartIndex = newIndex;
1261 }
1262 _flushInactiveSelections();
1263 return finalResult!;
1264 }
1265
1266 /// The compare function this delegate used for determining the selection
1267 /// order of the [Selectable]s.
1268 ///
1269 /// Sorts the [Selectable]s by their top left [Rect].
1270 @override
1271 Comparator<Selectable> get compareOrder => _compareScreenOrder;
1272
1273 static int _compareScreenOrder(Selectable a, Selectable b) {
1274 // Attempt to sort the selectables under a [_SelectableTextContainerDelegate]
1275 // by the top left rect.
1276 final Rect rectA = MatrixUtils.transformRect(a.getTransformTo(null), a.boundingBoxes.first);
1277 final Rect rectB = MatrixUtils.transformRect(b.getTransformTo(null), b.boundingBoxes.first);
1278 final int result = _compareVertically(rectA, rectB);
1279 if (result != 0) {
1280 return result;
1281 }
1282 return _compareHorizontally(rectA, rectB);
1283 }
1284
1285 /// Compares two rectangles in the screen order solely by their vertical
1286 /// positions.
1287 ///
1288 /// Returns positive if a is lower, negative if a is higher, 0 if their
1289 /// order can't be determine solely by their vertical position.
1290 static int _compareVertically(Rect a, Rect b) {
1291 // The rectangles overlap so defer to horizontal comparison.
1292 if ((a.top - b.top < _kSelectableVerticalComparingThreshold &&
1293 a.bottom - b.bottom > -_kSelectableVerticalComparingThreshold) ||
1294 (b.top - a.top < _kSelectableVerticalComparingThreshold &&
1295 b.bottom - a.bottom > -_kSelectableVerticalComparingThreshold)) {
1296 return 0;
1297 }
1298 if ((a.top - b.top).abs() > _kSelectableVerticalComparingThreshold) {
1299 return a.top > b.top ? 1 : -1;
1300 }
1301 return a.bottom > b.bottom ? 1 : -1;
1302 }
1303
1304 /// Compares two rectangles in the screen order by their horizontal positions
1305 /// assuming one of the rectangles enclose the other rect vertically.
1306 ///
1307 /// Returns positive if a is lower, negative if a is higher.
1308 static int _compareHorizontally(Rect a, Rect b) {
1309 // a encloses b.
1310 if (a.left - b.left < precisionErrorTolerance && a.right - b.right > -precisionErrorTolerance) {
1311 return -1;
1312 }
1313 // b encloses a.
1314 if (b.left - a.left < precisionErrorTolerance && b.right - a.right > -precisionErrorTolerance) {
1315 return 1;
1316 }
1317 if ((a.left - b.left).abs() > precisionErrorTolerance) {
1318 return a.left > b.left ? 1 : -1;
1319 }
1320 return a.right > b.right ? 1 : -1;
1321 }
1322
1323 /// This method calculates a local [SelectedContentRange] based on the list
1324 /// of [selections] that are accumulated from the [Selectable] children under this
1325 /// delegate. This calculation takes into account the accumulated content
1326 /// length before the active selection, and returns null when either selection
1327 /// edge has not been set.
1328 SelectedContentRange? _calculateLocalRange(List<_SelectionInfo> selections) {
1329 if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
1330 return null;
1331 }
1332 int startOffset = 0;
1333 int endOffset = 0;
1334 bool foundStart = false;
1335 bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex;
1336 if (currentSelectionEndIndex == currentSelectionStartIndex) {
1337 // Determining selection direction is inaccurate if currentSelectionStartIndex == currentSelectionEndIndex.
1338 // Use the range from the selectable within the selection as the source of truth for selection direction.
1339 final SelectedContentRange rangeAtSelectableInSelection =
1340 selectables[currentSelectionStartIndex].getSelection()!;
1341 forwardSelection =
1342 rangeAtSelectableInSelection.endOffset >= rangeAtSelectableInSelection.startOffset;
1343 }
1344 for (int index = 0; index < selections.length; index++) {
1345 final _SelectionInfo selection = selections[index];
1346 if (selection.range == null) {
1347 if (foundStart) {
1348 return SelectedContentRange(
1349 startOffset: forwardSelection ? startOffset : endOffset,
1350 endOffset: forwardSelection ? endOffset : startOffset,
1351 );
1352 }
1353 startOffset += selection.contentLength;
1354 endOffset = startOffset;
1355 continue;
1356 }
1357 final int selectionStartNormalized = min(
1358 selection.range!.startOffset,
1359 selection.range!.endOffset,
1360 );
1361 final int selectionEndNormalized = max(
1362 selection.range!.startOffset,
1363 selection.range!.endOffset,
1364 );
1365 if (!foundStart) {
1366 // Because a RenderParagraph may split its content into multiple selectables
1367 // we have to consider at what offset a selectable starts at relative
1368 // to the RenderParagraph, when the selectable is not the start of the content.
1369 final bool shouldConsiderContentStart =
1370 index > 0 && paragraph.selectableBelongsToParagraph(selectables[index]);
1371 startOffset +=
1372 (selectionStartNormalized -
1373 (shouldConsiderContentStart
1374 ? paragraph
1375 .getPositionForOffset(
1376 selectables[index].boundingBoxes.first.centerLeft,
1377 )
1378 .offset
1379 : 0))
1380 .abs();
1381 endOffset = startOffset + (selectionEndNormalized - selectionStartNormalized).abs();
1382 foundStart = true;
1383 } else {
1384 endOffset += (selectionEndNormalized - selectionStartNormalized).abs();
1385 }
1386 }
1387 assert(
1388 foundStart,
1389 'The start of the selection has not been found despite this selection delegate having an existing currentSelectionStartIndex and currentSelectionEndIndex.',
1390 );
1391 return SelectedContentRange(
1392 startOffset: forwardSelection ? startOffset : endOffset,
1393 endOffset: forwardSelection ? endOffset : startOffset,
1394 );
1395 }
1396
1397 /// Returns a [SelectedContentRange] considering the [SelectedContentRange]
1398 /// from each [Selectable] child managed under this delegate.
1399 ///
1400 /// When nothing is selected or either selection edge has not been set,
1401 /// this method will return `null`.
1402 @override
1403 SelectedContentRange? getSelection() {
1404 final List<_SelectionInfo> selections = <_SelectionInfo>[
1405 for (final Selectable selectable in selectables)
1406 (contentLength: selectable.contentLength, range: selectable.getSelection()),
1407 ];
1408 return _calculateLocalRange(selections);
1409 }
1410
1411 // From [SelectableRegion].
1412
1413 // Clears the selection on all selectables not in the range of
1414 // currentSelectionStartIndex..currentSelectionEndIndex.
1415 //
1416 // If one of the edges does not exist, then this method will clear the selection
1417 // in all selectables except the existing edge.
1418 //
1419 // If neither of the edges exist this method immediately returns.
1420 void _flushInactiveSelections() {
1421 if (currentSelectionStartIndex == -1 && currentSelectionEndIndex == -1) {
1422 return;
1423 }
1424 if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
1425 final int skipIndex = currentSelectionStartIndex == -1
1426 ? currentSelectionEndIndex
1427 : currentSelectionStartIndex;
1428 selectables
1429 .where((Selectable target) => target != selectables[skipIndex])
1430 .forEach(
1431 (Selectable target) =>
1432 dispatchSelectionEventToChild(target, const ClearSelectionEvent()),
1433 );
1434 return;
1435 }
1436 final int skipStart = min(currentSelectionStartIndex, currentSelectionEndIndex);
1437 final int skipEnd = max(currentSelectionStartIndex, currentSelectionEndIndex);
1438 for (int index = 0; index < selectables.length; index += 1) {
1439 if (index >= skipStart && index <= skipEnd) {
1440 continue;
1441 }
1442 dispatchSelectionEventToChild(selectables[index], const ClearSelectionEvent());
1443 }
1444 }
1445
1446 @override
1447 SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) {
1448 if (event.granularity != TextGranularity.paragraph) {
1449 return super.handleSelectionEdgeUpdate(event);
1450 }
1451 updateLastSelectionEdgeLocation(
1452 globalSelectionEdgeLocation: event.globalPosition,
1453 forEnd: event.type == SelectionEventType.endEdgeUpdate,
1454 );
1455 if (event.type == SelectionEventType.endEdgeUpdate) {
1456 return currentSelectionEndIndex == -1
1457 ? _initSelection(event, isEnd: true)
1458 : _adjustSelection(event, isEnd: true);
1459 }
1460 return currentSelectionStartIndex == -1
1461 ? _initSelection(event, isEnd: false)
1462 : _adjustSelection(event, isEnd: false);
1463 }
1464}
1465
1466/// The length of the content that can be selected, and the range that is
1467/// selected.
1468typedef _SelectionInfo = ({int contentLength, SelectedContentRange? range});
1469