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 children: textSpan != null ? <InlineSpan>[textSpan!] : null,
739 ),
740 ),
741 );
742 } else {
743 result = RichText(
744 textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
745 textDirection:
746 textDirection, // RichText uses Directionality.of to obtain a default if this is null.
747 locale: locale, // RichText uses Localizations.localeOf to obtain a default if this is null
748 softWrap: softWrap ?? defaultTextStyle.softWrap,
749 overflow: overflow ?? effectiveTextStyle?.overflow ?? defaultTextStyle.overflow,
750 textScaler: textScaler,
751 maxLines: maxLines ?? defaultTextStyle.maxLines,
752 strutStyle: strutStyle,
753 textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
754 textHeightBehavior:
755 textHeightBehavior ??
756 defaultTextStyle.textHeightBehavior ??
757 DefaultTextHeightBehavior.maybeOf(context),
758 selectionColor:
759 selectionColor ??
760 DefaultSelectionStyle.of(context).selectionColor ??
761 DefaultSelectionStyle.defaultColor,
762 text: TextSpan(
763 style: effectiveTextStyle,
764 text: data,
765 children: textSpan != null ? <InlineSpan>[textSpan!] : null,
766 ),
767 );
768 }
769 if (semanticsLabel != null || semanticsIdentifier != null) {
770 result = Semantics(
771 textDirection: textDirection,
772 label: semanticsLabel,
773 identifier: semanticsIdentifier,
774 child: ExcludeSemantics(excluding: semanticsLabel != null, child: result),
775 );
776 }
777 return result;
778 }
779
780 @override
781 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
782 super.debugFillProperties(properties);
783 properties.add(StringProperty('data', data, showName: false));
784 if (textSpan != null) {
785 properties.add(
786 textSpan!.toDiagnosticsNode(name: 'textSpan', style: DiagnosticsTreeStyle.transition),
787 );
788 }
789 style?.debugFillProperties(properties);
790 properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
791 properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
792 properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
793 properties.add(
794 FlagProperty(
795 'softWrap',
796 value: softWrap,
797 ifTrue: 'wrapping at box width',
798 ifFalse: 'no wrapping except at line break characters',
799 showName: true,
800 ),
801 );
802 properties.add(EnumProperty<TextOverflow>('overflow', overflow, defaultValue: null));
803 properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
804 properties.add(IntProperty('maxLines', maxLines, defaultValue: null));
805 properties.add(
806 EnumProperty<TextWidthBasis>('textWidthBasis', textWidthBasis, defaultValue: null),
807 );
808 properties.add(
809 DiagnosticsProperty<ui.TextHeightBehavior>(
810 'textHeightBehavior',
811 textHeightBehavior,
812 defaultValue: null,
813 ),
814 );
815 if (semanticsLabel != null) {
816 properties.add(StringProperty('semanticsLabel', semanticsLabel));
817 }
818 if (semanticsIdentifier != null) {
819 properties.add(StringProperty('semanticsIdentifier', semanticsIdentifier));
820 }
821 }
822}
823
824class _SelectableTextContainer extends StatefulWidget {
825 const _SelectableTextContainer({
826 required this.text,
827 required this.textAlign,
828 this.textDirection,
829 required this.softWrap,
830 required this.overflow,
831 required this.textScaler,
832 this.maxLines,
833 this.locale,
834 this.strutStyle,
835 required this.textWidthBasis,
836 this.textHeightBehavior,
837 required this.selectionColor,
838 });
839
840 final TextSpan text;
841 final TextAlign textAlign;
842 final TextDirection? textDirection;
843 final bool softWrap;
844 final TextOverflow overflow;
845 final TextScaler textScaler;
846 final int? maxLines;
847 final Locale? locale;
848 final StrutStyle? strutStyle;
849 final TextWidthBasis textWidthBasis;
850 final ui.TextHeightBehavior? textHeightBehavior;
851 final Color selectionColor;
852
853 @override
854 State<_SelectableTextContainer> createState() => _SelectableTextContainerState();
855}
856
857class _SelectableTextContainerState extends State<_SelectableTextContainer> {
858 late final _SelectableTextContainerDelegate _selectionDelegate;
859 final GlobalKey _textKey = GlobalKey();
860
861 @override
862 void initState() {
863 super.initState();
864 _selectionDelegate = _SelectableTextContainerDelegate(_textKey);
865 }
866
867 @override
868 void dispose() {
869 _selectionDelegate.dispose();
870 super.dispose();
871 }
872
873 @override
874 Widget build(BuildContext context) {
875 return SelectionContainer(
876 delegate: _selectionDelegate,
877 // Use [_RichText] wrapper so the underlying [RenderParagraph] can register
878 // its [Selectable]s to the [SelectionContainer] created by this widget.
879 child: _RichText(
880 textKey: _textKey,
881 textAlign: widget.textAlign,
882 textDirection: widget.textDirection,
883 locale: widget.locale,
884 softWrap: widget.softWrap,
885 overflow: widget.overflow,
886 textScaler: widget.textScaler,
887 maxLines: widget.maxLines,
888 strutStyle: widget.strutStyle,
889 textWidthBasis: widget.textWidthBasis,
890 textHeightBehavior: widget.textHeightBehavior,
891 selectionColor: widget.selectionColor,
892 text: widget.text,
893 ),
894 );
895 }
896}
897
898class _RichText extends StatelessWidget {
899 const _RichText({
900 this.textKey,
901 required this.text,
902 required this.textAlign,
903 this.textDirection,
904 required this.softWrap,
905 required this.overflow,
906 required this.textScaler,
907 this.maxLines,
908 this.locale,
909 this.strutStyle,
910 required this.textWidthBasis,
911 this.textHeightBehavior,
912 required this.selectionColor,
913 });
914
915 final GlobalKey? textKey;
916 final InlineSpan text;
917 final TextAlign textAlign;
918 final TextDirection? textDirection;
919 final bool softWrap;
920 final TextOverflow overflow;
921 final TextScaler textScaler;
922 final int? maxLines;
923 final Locale? locale;
924 final StrutStyle? strutStyle;
925 final TextWidthBasis textWidthBasis;
926 final ui.TextHeightBehavior? textHeightBehavior;
927 final Color selectionColor;
928
929 @override
930 Widget build(BuildContext context) {
931 final SelectionRegistrar? registrar = SelectionContainer.maybeOf(context);
932 return RichText(
933 key: textKey,
934 textAlign: textAlign,
935 textDirection: textDirection,
936 locale: locale,
937 softWrap: softWrap,
938 overflow: overflow,
939 textScaler: textScaler,
940 maxLines: maxLines,
941 strutStyle: strutStyle,
942 textWidthBasis: textWidthBasis,
943 textHeightBehavior: textHeightBehavior,
944 selectionRegistrar: registrar,
945 selectionColor: selectionColor,
946 text: text,
947 );
948 }
949}
950
951// In practice some selectables like widgetspan shift several pixels. So when
952// the vertical position diff is within the threshold, compare the horizontal
953// position to make the compareScreenOrder function more robust.
954const double _kSelectableVerticalComparingThreshold = 3.0;
955
956class _SelectableTextContainerDelegate extends StaticSelectionContainerDelegate {
957 _SelectableTextContainerDelegate(GlobalKey textKey) : _textKey = textKey;
958
959 final GlobalKey _textKey;
960 RenderParagraph get paragraph => _textKey.currentContext!.findRenderObject()! as RenderParagraph;
961
962 @override
963 SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) {
964 final SelectionResult result = _handleSelectParagraph(event);
965 super.didReceiveSelectionBoundaryEvents();
966 return result;
967 }
968
969 SelectionResult _handleSelectParagraph(SelectParagraphSelectionEvent event) {
970 if (event.absorb) {
971 for (int index = 0; index < selectables.length; index += 1) {
972 dispatchSelectionEventToChild(selectables[index], event);
973 }
974 currentSelectionStartIndex = 0;
975 currentSelectionEndIndex = selectables.length - 1;
976 return SelectionResult.next;
977 }
978
979 // First pass, if the position is on a placeholder then dispatch the selection
980 // event to the [Selectable] at the location and terminate.
981 for (int index = 0; index < selectables.length; index += 1) {
982 final bool selectableIsPlaceholder =
983 !paragraph.selectableBelongsToParagraph(selectables[index]);
984 if (selectableIsPlaceholder && selectables[index].boundingBoxes.isNotEmpty) {
985 for (final Rect rect in selectables[index].boundingBoxes) {
986 final Rect globalRect = MatrixUtils.transformRect(
987 selectables[index].getTransformTo(null),
988 rect,
989 );
990 if (globalRect.contains(event.globalPosition)) {
991 currentSelectionStartIndex = currentSelectionEndIndex = index;
992 return dispatchSelectionEventToChild(selectables[index], event);
993 }
994 }
995 }
996 }
997
998 SelectionResult? lastSelectionResult;
999 bool foundStart = false;
1000 int? lastNextIndex;
1001 for (int index = 0; index < selectables.length; index += 1) {
1002 if (!paragraph.selectableBelongsToParagraph(selectables[index])) {
1003 if (foundStart) {
1004 final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent(
1005 globalPosition: event.globalPosition,
1006 absorb: true,
1007 );
1008 final SelectionResult result = dispatchSelectionEventToChild(
1009 selectables[index],
1010 synthesizedEvent,
1011 );
1012 if (selectables.length - 1 == index) {
1013 currentSelectionEndIndex = index;
1014 _flushInactiveSelections();
1015 return result;
1016 }
1017 }
1018 continue;
1019 }
1020 final SelectionGeometry existingGeometry = selectables[index].value;
1021 lastSelectionResult = dispatchSelectionEventToChild(selectables[index], event);
1022 if (index == selectables.length - 1 && lastSelectionResult == SelectionResult.next) {
1023 if (foundStart) {
1024 currentSelectionEndIndex = index;
1025 } else {
1026 currentSelectionStartIndex = currentSelectionEndIndex = index;
1027 }
1028 return SelectionResult.next;
1029 }
1030 if (lastSelectionResult == SelectionResult.next) {
1031 if (selectables[index].value == existingGeometry && !foundStart) {
1032 lastNextIndex = index;
1033 }
1034 if (selectables[index].value != existingGeometry && !foundStart) {
1035 assert(selectables[index].boundingBoxes.isNotEmpty);
1036 assert(selectables[index].value.selectionRects.isNotEmpty);
1037 final bool selectionAtStartOfSelectable = selectables[index].boundingBoxes[0].overlaps(
1038 selectables[index].value.selectionRects[0],
1039 );
1040 int startIndex = 0;
1041 if (lastNextIndex != null && selectionAtStartOfSelectable) {
1042 startIndex = lastNextIndex + 1;
1043 } else {
1044 startIndex = lastNextIndex == null && selectionAtStartOfSelectable ? 0 : index;
1045 }
1046 for (int i = startIndex; i < index; i += 1) {
1047 final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent(
1048 globalPosition: event.globalPosition,
1049 absorb: true,
1050 );
1051 dispatchSelectionEventToChild(selectables[i], synthesizedEvent);
1052 }
1053 currentSelectionStartIndex = startIndex;
1054 foundStart = true;
1055 }
1056 continue;
1057 }
1058 if (index == 0 && lastSelectionResult == SelectionResult.previous) {
1059 return SelectionResult.previous;
1060 }
1061 if (selectables[index].value != existingGeometry) {
1062 if (!foundStart && lastNextIndex == null) {
1063 currentSelectionStartIndex = 0;
1064 for (int i = 0; i < index; i += 1) {
1065 final SelectionEvent synthesizedEvent = SelectParagraphSelectionEvent(
1066 globalPosition: event.globalPosition,
1067 absorb: true,
1068 );
1069 dispatchSelectionEventToChild(selectables[i], synthesizedEvent);
1070 }
1071 }
1072 currentSelectionEndIndex = index;
1073 // Geometry has changed as a result of select paragraph, need to clear the
1074 // selection of other selectables to keep selection in sync.
1075 _flushInactiveSelections();
1076 }
1077 return SelectionResult.end;
1078 }
1079 assert(lastSelectionResult == null);
1080 return SelectionResult.end;
1081 }
1082
1083 /// Initializes the selection of the selectable children.
1084 ///
1085 /// The goal is to find the selectable child that contains the selection edge.
1086 /// Returns [SelectionResult.end] if the selection edge ends on any of the
1087 /// children. Otherwise, it returns [SelectionResult.previous] if the selection
1088 /// does not reach any of its children. Returns [SelectionResult.next]
1089 /// if the selection reaches the end of its children.
1090 ///
1091 /// Ideally, this method should only be called twice at the beginning of the
1092 /// drag selection, once for start edge update event, once for end edge update
1093 /// event.
1094 SelectionResult _initSelection(SelectionEdgeUpdateEvent event, {required bool isEnd}) {
1095 assert(
1096 (isEnd && currentSelectionEndIndex == -1) || (!isEnd && currentSelectionStartIndex == -1),
1097 );
1098 SelectionResult? finalResult;
1099 // Begin the search for the selection edge at the opposite edge if it exists.
1100 final bool hasOppositeEdge =
1101 isEnd ? currentSelectionStartIndex != -1 : currentSelectionEndIndex != -1;
1102 int newIndex = switch ((isEnd, hasOppositeEdge)) {
1103 (true, true) => currentSelectionStartIndex,
1104 (true, false) => 0,
1105 (false, true) => currentSelectionEndIndex,
1106 (false, false) => 0,
1107 };
1108 bool? forward;
1109 late SelectionResult currentSelectableResult;
1110 // This loop sends the selection event to one of the following to determine
1111 // the direction of the search.
1112 // - The opposite edge index if it exists.
1113 // - Index 0 if the opposite edge index does not exist.
1114 //
1115 // If the result is `SelectionResult.next`, this loop look backward.
1116 // Otherwise, it looks forward.
1117 //
1118 // The terminate condition are:
1119 // 1. the selectable returns end, pending, none.
1120 // 2. the selectable returns previous when looking forward.
1121 // 2. the selectable returns next when looking backward.
1122 while (newIndex < selectables.length && newIndex >= 0 && finalResult == null) {
1123 currentSelectableResult = dispatchSelectionEventToChild(selectables[newIndex], event);
1124 switch (currentSelectableResult) {
1125 case SelectionResult.end:
1126 case SelectionResult.pending:
1127 case SelectionResult.none:
1128 finalResult = currentSelectableResult;
1129 case SelectionResult.next:
1130 if (forward == false) {
1131 newIndex += 1;
1132 finalResult = SelectionResult.end;
1133 } else if (newIndex == selectables.length - 1) {
1134 finalResult = currentSelectableResult;
1135 } else {
1136 forward = true;
1137 newIndex += 1;
1138 }
1139 case SelectionResult.previous:
1140 if (forward ?? false) {
1141 newIndex -= 1;
1142 finalResult = SelectionResult.end;
1143 } else if (newIndex == 0) {
1144 finalResult = currentSelectableResult;
1145 } else {
1146 forward = false;
1147 newIndex -= 1;
1148 }
1149 }
1150 }
1151 if (isEnd) {
1152 currentSelectionEndIndex = newIndex;
1153 } else {
1154 currentSelectionStartIndex = newIndex;
1155 }
1156 _flushInactiveSelections();
1157 return finalResult!;
1158 }
1159
1160 SelectionResult _adjustSelection(SelectionEdgeUpdateEvent event, {required bool isEnd}) {
1161 assert(() {
1162 if (isEnd) {
1163 assert(currentSelectionEndIndex < selectables.length && currentSelectionEndIndex >= 0);
1164 return true;
1165 }
1166 assert(currentSelectionStartIndex < selectables.length && currentSelectionStartIndex >= 0);
1167 return true;
1168 }());
1169 SelectionResult? finalResult;
1170 // Determines if the edge being adjusted is within the current viewport.
1171 // - If so, we begin the search for the new selection edge position at the
1172 // currentSelectionEndIndex/currentSelectionStartIndex.
1173 // - If not, we attempt to locate the new selection edge starting from
1174 // the opposite end.
1175 // - If neither edge is in the current viewport, the search for the new
1176 // selection edge position begins at 0.
1177 //
1178 // This can happen when there is a scrollable child and the edge being adjusted
1179 // has been scrolled out of view.
1180 final bool isCurrentEdgeWithinViewport =
1181 isEnd ? value.endSelectionPoint != null : value.startSelectionPoint != null;
1182 final bool isOppositeEdgeWithinViewport =
1183 isEnd ? value.startSelectionPoint != null : value.endSelectionPoint != null;
1184 int newIndex = switch ((isEnd, isCurrentEdgeWithinViewport, isOppositeEdgeWithinViewport)) {
1185 (true, true, true) => currentSelectionEndIndex,
1186 (true, true, false) => currentSelectionEndIndex,
1187 (true, false, true) => currentSelectionStartIndex,
1188 (true, false, false) => 0,
1189 (false, true, true) => currentSelectionStartIndex,
1190 (false, true, false) => currentSelectionStartIndex,
1191 (false, false, true) => currentSelectionEndIndex,
1192 (false, false, false) => 0,
1193 };
1194 bool? forward;
1195 late SelectionResult currentSelectableResult;
1196 // This loop sends the selection event to one of the following to determine
1197 // the direction of the search.
1198 // - currentSelectionEndIndex/currentSelectionStartIndex if the current edge
1199 // is in the current viewport.
1200 // - The opposite edge index if the current edge is not in the current viewport.
1201 // - Index 0 if neither edge is in the current viewport.
1202 //
1203 // If the result is `SelectionResult.next`, this loop look backward.
1204 // Otherwise, it looks forward.
1205 //
1206 // The terminate condition are:
1207 // 1. the selectable returns end, pending, none.
1208 // 2. the selectable returns previous when looking forward.
1209 // 2. the selectable returns next when looking backward.
1210 while (newIndex < selectables.length && newIndex >= 0 && finalResult == null) {
1211 currentSelectableResult = dispatchSelectionEventToChild(selectables[newIndex], event);
1212 switch (currentSelectableResult) {
1213 case SelectionResult.end:
1214 case SelectionResult.pending:
1215 case SelectionResult.none:
1216 finalResult = currentSelectableResult;
1217 case SelectionResult.next:
1218 if (forward == false) {
1219 newIndex += 1;
1220 finalResult = SelectionResult.end;
1221 } else if (newIndex == selectables.length - 1) {
1222 finalResult = currentSelectableResult;
1223 } else {
1224 forward = true;
1225 newIndex += 1;
1226 }
1227 case SelectionResult.previous:
1228 if (forward ?? false) {
1229 newIndex -= 1;
1230 finalResult = SelectionResult.end;
1231 } else if (newIndex == 0) {
1232 finalResult = currentSelectableResult;
1233 } else {
1234 forward = false;
1235 newIndex -= 1;
1236 }
1237 }
1238 }
1239 if (isEnd) {
1240 final bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex;
1241 if (forward != null &&
1242 ((!forwardSelection && forward && newIndex >= currentSelectionStartIndex) ||
1243 (forwardSelection && !forward && newIndex <= currentSelectionStartIndex))) {
1244 currentSelectionStartIndex = currentSelectionEndIndex;
1245 }
1246 currentSelectionEndIndex = newIndex;
1247 } else {
1248 final bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex;
1249 if (forward != null &&
1250 ((!forwardSelection && !forward && newIndex <= currentSelectionEndIndex) ||
1251 (forwardSelection && forward && newIndex >= currentSelectionEndIndex))) {
1252 currentSelectionEndIndex = currentSelectionStartIndex;
1253 }
1254 currentSelectionStartIndex = newIndex;
1255 }
1256 _flushInactiveSelections();
1257 return finalResult!;
1258 }
1259
1260 /// The compare function this delegate used for determining the selection
1261 /// order of the [Selectable]s.
1262 ///
1263 /// Sorts the [Selectable]s by their top left [Rect].
1264 @override
1265 Comparator<Selectable> get compareOrder => _compareScreenOrder;
1266
1267 static int _compareScreenOrder(Selectable a, Selectable b) {
1268 // Attempt to sort the selectables under a [_SelectableTextContainerDelegate]
1269 // by the top left rect.
1270 final Rect rectA = MatrixUtils.transformRect(a.getTransformTo(null), a.boundingBoxes.first);
1271 final Rect rectB = MatrixUtils.transformRect(b.getTransformTo(null), b.boundingBoxes.first);
1272 final int result = _compareVertically(rectA, rectB);
1273 if (result != 0) {
1274 return result;
1275 }
1276 return _compareHorizontally(rectA, rectB);
1277 }
1278
1279 /// Compares two rectangles in the screen order solely by their vertical
1280 /// positions.
1281 ///
1282 /// Returns positive if a is lower, negative if a is higher, 0 if their
1283 /// order can't be determine solely by their vertical position.
1284 static int _compareVertically(Rect a, Rect b) {
1285 // The rectangles overlap so defer to horizontal comparison.
1286 if ((a.top - b.top < _kSelectableVerticalComparingThreshold &&
1287 a.bottom - b.bottom > -_kSelectableVerticalComparingThreshold) ||
1288 (b.top - a.top < _kSelectableVerticalComparingThreshold &&
1289 b.bottom - a.bottom > -_kSelectableVerticalComparingThreshold)) {
1290 return 0;
1291 }
1292 if ((a.top - b.top).abs() > _kSelectableVerticalComparingThreshold) {
1293 return a.top > b.top ? 1 : -1;
1294 }
1295 return a.bottom > b.bottom ? 1 : -1;
1296 }
1297
1298 /// Compares two rectangles in the screen order by their horizontal positions
1299 /// assuming one of the rectangles enclose the other rect vertically.
1300 ///
1301 /// Returns positive if a is lower, negative if a is higher.
1302 static int _compareHorizontally(Rect a, Rect b) {
1303 // a encloses b.
1304 if (a.left - b.left < precisionErrorTolerance && a.right - b.right > -precisionErrorTolerance) {
1305 return -1;
1306 }
1307 // b encloses a.
1308 if (b.left - a.left < precisionErrorTolerance && b.right - a.right > -precisionErrorTolerance) {
1309 return 1;
1310 }
1311 if ((a.left - b.left).abs() > precisionErrorTolerance) {
1312 return a.left > b.left ? 1 : -1;
1313 }
1314 return a.right > b.right ? 1 : -1;
1315 }
1316
1317 /// This method calculates a local [SelectedContentRange] based on the list
1318 /// of [selections] that are accumulated from the [Selectable] children under this
1319 /// delegate. This calculation takes into account the accumulated content
1320 /// length before the active selection, and returns null when either selection
1321 /// edge has not been set.
1322 SelectedContentRange? _calculateLocalRange(List<_SelectionInfo> selections) {
1323 if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
1324 return null;
1325 }
1326 int startOffset = 0;
1327 int endOffset = 0;
1328 bool foundStart = false;
1329 bool forwardSelection = currentSelectionEndIndex >= currentSelectionStartIndex;
1330 if (currentSelectionEndIndex == currentSelectionStartIndex) {
1331 // Determining selection direction is innacurate if currentSelectionStartIndex == currentSelectionEndIndex.
1332 // Use the range from the selectable within the selection as the source of truth for selection direction.
1333 final SelectedContentRange rangeAtSelectableInSelection =
1334 selectables[currentSelectionStartIndex].getSelection()!;
1335 forwardSelection =
1336 rangeAtSelectableInSelection.endOffset >= rangeAtSelectableInSelection.startOffset;
1337 }
1338 for (int index = 0; index < selections.length; index++) {
1339 final _SelectionInfo selection = selections[index];
1340 if (selection.range == null) {
1341 if (foundStart) {
1342 return SelectedContentRange(
1343 startOffset: forwardSelection ? startOffset : endOffset,
1344 endOffset: forwardSelection ? endOffset : startOffset,
1345 );
1346 }
1347 startOffset += selection.contentLength;
1348 endOffset = startOffset;
1349 continue;
1350 }
1351 final int selectionStartNormalized = min(
1352 selection.range!.startOffset,
1353 selection.range!.endOffset,
1354 );
1355 final int selectionEndNormalized = max(
1356 selection.range!.startOffset,
1357 selection.range!.endOffset,
1358 );
1359 if (!foundStart) {
1360 // Because a RenderParagraph may split its content into multiple selectables
1361 // we have to consider at what offset a selectable starts at relative
1362 // to the RenderParagraph, when the selectable is not the start of the content.
1363 final bool shouldConsiderContentStart =
1364 index > 0 && paragraph.selectableBelongsToParagraph(selectables[index]);
1365 startOffset +=
1366 (selectionStartNormalized -
1367 (shouldConsiderContentStart
1368 ? paragraph
1369 .getPositionForOffset(selectables[index].boundingBoxes.first.centerLeft)
1370 .offset
1371 : 0))
1372 .abs();
1373 endOffset = startOffset + (selectionEndNormalized - selectionStartNormalized).abs();
1374 foundStart = true;
1375 } else {
1376 endOffset += (selectionEndNormalized - selectionStartNormalized).abs();
1377 }
1378 }
1379 assert(
1380 foundStart,
1381 'The start of the selection has not been found despite this selection delegate having an existing currentSelectionStartIndex and currentSelectionEndIndex.',
1382 );
1383 return SelectedContentRange(
1384 startOffset: forwardSelection ? startOffset : endOffset,
1385 endOffset: forwardSelection ? endOffset : startOffset,
1386 );
1387 }
1388
1389 /// Returns a [SelectedContentRange] considering the [SelectedContentRange]
1390 /// from each [Selectable] child managed under this delegate.
1391 ///
1392 /// When nothing is selected or either selection edge has not been set,
1393 /// this method will return `null`.
1394 @override
1395 SelectedContentRange? getSelection() {
1396 final List<_SelectionInfo> selections = <_SelectionInfo>[
1397 for (final Selectable selectable in selectables)
1398 (contentLength: selectable.contentLength, range: selectable.getSelection()),
1399 ];
1400 return _calculateLocalRange(selections);
1401 }
1402
1403 // From [SelectableRegion].
1404
1405 // Clears the selection on all selectables not in the range of
1406 // currentSelectionStartIndex..currentSelectionEndIndex.
1407 //
1408 // If one of the edges does not exist, then this method will clear the selection
1409 // in all selectables except the existing edge.
1410 //
1411 // If neither of the edges exist this method immediately returns.
1412 void _flushInactiveSelections() {
1413 if (currentSelectionStartIndex == -1 && currentSelectionEndIndex == -1) {
1414 return;
1415 }
1416 if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
1417 final int skipIndex =
1418 currentSelectionStartIndex == -1 ? currentSelectionEndIndex : currentSelectionStartIndex;
1419 selectables
1420 .where((Selectable target) => target != selectables[skipIndex])
1421 .forEach(
1422 (Selectable target) =>
1423 dispatchSelectionEventToChild(target, const ClearSelectionEvent()),
1424 );
1425 return;
1426 }
1427 final int skipStart = min(currentSelectionStartIndex, currentSelectionEndIndex);
1428 final int skipEnd = max(currentSelectionStartIndex, currentSelectionEndIndex);
1429 for (int index = 0; index < selectables.length; index += 1) {
1430 if (index >= skipStart && index <= skipEnd) {
1431 continue;
1432 }
1433 dispatchSelectionEventToChild(selectables[index], const ClearSelectionEvent());
1434 }
1435 }
1436
1437 @override
1438 SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) {
1439 if (event.granularity != TextGranularity.paragraph) {
1440 return super.handleSelectionEdgeUpdate(event);
1441 }
1442 updateLastSelectionEdgeLocation(
1443 globalSelectionEdgeLocation: event.globalPosition,
1444 forEnd: event.type == SelectionEventType.endEdgeUpdate,
1445 );
1446 if (event.type == SelectionEventType.endEdgeUpdate) {
1447 return currentSelectionEndIndex == -1
1448 ? _initSelection(event, isEnd: true)
1449 : _adjustSelection(event, isEnd: true);
1450 }
1451 return currentSelectionStartIndex == -1
1452 ? _initSelection(event, isEnd: false)
1453 : _adjustSelection(event, isEnd: false);
1454 }
1455}
1456
1457/// The length of the content that can be selected, and the range that is
1458/// selected.
1459typedef _SelectionInfo = ({int contentLength, SelectedContentRange? range});
1460

Provided by KDAB

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