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 'dart:ui';
6///
7/// @docImport 'package:flutter/rendering.dart';
8/// @docImport 'package:flutter/widgets.dart';
9library;
10
11import 'dart:ui'
12 as ui
13 show Locale, LocaleStringAttribute, ParagraphBuilder, SpellOutStringAttribute, StringAttribute;
14
15import 'package:flutter/foundation.dart';
16import 'package:flutter/gestures.dart';
17import 'package:flutter/services.dart';
18
19import 'basic_types.dart';
20import 'inline_span.dart';
21import 'text_painter.dart';
22import 'text_scaler.dart';
23
24// Examples can assume:
25// late TextSpan myTextSpan;
26
27/// An immutable span of text.
28///
29/// A [TextSpan] object can be styled using its [style] property. The style will
30/// be applied to the [text] and the [children].
31///
32/// A [TextSpan] object can just have plain text, or it can have children
33/// [TextSpan] objects with their own styles that (possibly only partially)
34/// override the [style] of this object. If a [TextSpan] has both [text] and
35/// [children], then the [text] is treated as if it was an un-styled [TextSpan]
36/// at the start of the [children] list. Leaving the [TextSpan.text] field null
37/// results in the [TextSpan] acting as an empty node in the [InlineSpan] tree
38/// with a list of children.
39///
40/// To paint a [TextSpan] on a [Canvas], use a [TextPainter]. To display a text
41/// span in a widget, use a [RichText]. For text with a single style, consider
42/// using the [Text] widget.
43///
44/// {@tool snippet}
45///
46/// The text "Hello world!", in black:
47///
48/// ```dart
49/// const TextSpan(
50/// text: 'Hello world!',
51/// style: TextStyle(color: Colors.black),
52/// )
53/// ```
54/// {@end-tool}
55///
56/// _There is some more detailed sample code in the documentation for the
57/// [recognizer] property._
58///
59/// The [TextSpan.text] will be used as the semantics label unless overridden
60/// by the [TextSpan.semanticsLabel] property. Any [PlaceholderSpan]s in the
61/// [TextSpan.children] list will separate the text before and after it into two
62/// semantics nodes.
63///
64/// See also:
65///
66/// * [WidgetSpan], a leaf node that represents an embedded inline widget in an
67/// [InlineSpan] tree. Specify a widget within the [children] list by
68/// wrapping the widget with a [WidgetSpan]. The widget will be laid out
69/// inline within the paragraph.
70/// * [Text], a widget for showing uniformly-styled text.
71/// * [RichText], a widget for finer control of text rendering.
72/// * [TextPainter], a class for painting [TextSpan] objects on a [Canvas].
73@immutable
74class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotation {
75 /// Creates a [TextSpan] with the given values.
76 ///
77 /// For the object to be useful, at least one of [text] or
78 /// [children] should be set.
79 const TextSpan({
80 this.text,
81 this.children,
82 super.style,
83 this.recognizer,
84 MouseCursor? mouseCursor,
85 this.onEnter,
86 this.onExit,
87 this.semanticsLabel,
88 this.semanticsIdentifier,
89 this.locale,
90 this.spellOut,
91 }) : mouseCursor =
92 mouseCursor ?? (recognizer == null ? MouseCursor.defer : SystemMouseCursors.click),
93 assert(!(text == null && semanticsLabel != null));
94
95 /// The text contained in this span.
96 ///
97 /// If both [text] and [children] are non-null, the text will precede the
98 /// children.
99 ///
100 /// This getter does not include the contents of its children.
101 final String? text;
102
103 /// Additional spans to include as children.
104 ///
105 /// If both [text] and [children] are non-null, the text will precede the
106 /// children.
107 ///
108 /// Modifying the list after the [TextSpan] has been created is not supported
109 /// and may have unexpected results.
110 ///
111 /// The list must not contain any nulls.
112 final List<InlineSpan>? children;
113
114 /// A gesture recognizer that will receive events that hit this span.
115 ///
116 /// [InlineSpan] itself does not implement hit testing or event dispatch. The
117 /// object that manages the [InlineSpan] painting is also responsible for
118 /// dispatching events. In the rendering library, that is the
119 /// [RenderParagraph] object, which corresponds to the [RichText] widget in
120 /// the widgets layer; these objects do not bubble events in [InlineSpan]s,
121 /// so a [recognizer] is only effective for events that directly hit the
122 /// [text] of that [InlineSpan], not any of its [children].
123 ///
124 /// [InlineSpan] also does not manage the lifetime of the gesture recognizer.
125 /// The code that owns the [GestureRecognizer] object must call
126 /// [GestureRecognizer.dispose] when the [InlineSpan] object is no longer
127 /// used.
128 ///
129 /// {@tool snippet}
130 ///
131 /// This example shows how to manage the lifetime of a gesture recognizer
132 /// provided to an [InlineSpan] object. It defines a `BuzzingText` widget
133 /// which uses the [HapticFeedback] class to vibrate the device when the user
134 /// long-presses the "find the" span, which is underlined in wavy green. The
135 /// hit-testing is handled by the [RichText] widget. It also changes the
136 /// hovering mouse cursor to `precise`.
137 ///
138 /// ```dart
139 /// class BuzzingText extends StatefulWidget {
140 /// const BuzzingText({super.key});
141 ///
142 /// @override
143 /// State<BuzzingText> createState() => _BuzzingTextState();
144 /// }
145 ///
146 /// class _BuzzingTextState extends State<BuzzingText> {
147 /// late LongPressGestureRecognizer _longPressRecognizer;
148 ///
149 /// @override
150 /// void initState() {
151 /// super.initState();
152 /// _longPressRecognizer = LongPressGestureRecognizer()
153 /// ..onLongPress = _handlePress;
154 /// }
155 ///
156 /// @override
157 /// void dispose() {
158 /// _longPressRecognizer.dispose();
159 /// super.dispose();
160 /// }
161 ///
162 /// void _handlePress() {
163 /// HapticFeedback.vibrate();
164 /// }
165 ///
166 /// @override
167 /// Widget build(BuildContext context) {
168 /// return Text.rich(
169 /// TextSpan(
170 /// text: 'Can you ',
171 /// style: const TextStyle(color: Colors.black),
172 /// children: <InlineSpan>[
173 /// TextSpan(
174 /// text: 'find the',
175 /// style: const TextStyle(
176 /// color: Colors.green,
177 /// decoration: TextDecoration.underline,
178 /// decorationStyle: TextDecorationStyle.wavy,
179 /// ),
180 /// recognizer: _longPressRecognizer,
181 /// mouseCursor: SystemMouseCursors.precise,
182 /// ),
183 /// const TextSpan(
184 /// text: ' secret?',
185 /// ),
186 /// ],
187 /// ),
188 /// );
189 /// }
190 /// }
191 /// ```
192 /// {@end-tool}
193 final GestureRecognizer? recognizer;
194
195 /// Mouse cursor when the mouse hovers over this span.
196 ///
197 /// The default value is [SystemMouseCursors.click] if [recognizer] is not
198 /// null, or [MouseCursor.defer] otherwise.
199 ///
200 /// [TextSpan] itself does not implement hit testing or cursor changing.
201 /// The object that manages the [TextSpan] painting is responsible
202 /// to return the [TextSpan] in its hit test, as well as providing the
203 /// correct mouse cursor when the [TextSpan]'s mouse cursor is
204 /// [MouseCursor.defer].
205 final MouseCursor mouseCursor;
206
207 @override
208 final PointerEnterEventListener? onEnter;
209
210 @override
211 final PointerExitEventListener? onExit;
212
213 /// Returns the value of [mouseCursor].
214 ///
215 /// This field, required by [MouseTrackerAnnotation], is hidden publicly to
216 /// avoid the confusion as a text cursor.
217 @protected
218 @override
219 MouseCursor get cursor => mouseCursor;
220
221 /// An alternative semantics label for this [TextSpan].
222 ///
223 /// If present, the semantics of this span will contain this value instead
224 /// of the actual text.
225 ///
226 /// This is useful for replacing abbreviations or shorthands with the full
227 /// text value:
228 ///
229 /// ```dart
230 /// const TextSpan(text: r'$$', semanticsLabel: 'Double dollars')
231 /// ```
232 final String? semanticsLabel;
233
234 /// A unique identifier for the semantics node for this [TextSpan].
235 ///
236 /// This is useful for cases where the text content of the [TextSpan] needs
237 /// to be uniquely identified through the automation tools without having
238 /// a dependency on the actual content of the text that can possibly be
239 /// dynamic in nature.
240 final String? semanticsIdentifier;
241
242 /// The language of the text in this span and its span children.
243 ///
244 /// Setting the locale of this text span affects the way that assistive
245 /// technologies, such as VoiceOver or TalkBack, pronounce the text.
246 ///
247 /// If this span contains other text span children, they also inherit the
248 /// locale from this span unless explicitly set to different locales.
249 final ui.Locale? locale;
250
251 /// Whether the assistive technologies should spell out this text character
252 /// by character.
253 ///
254 /// If the text is 'hello world', setting this to true causes the assistive
255 /// technologies, such as VoiceOver or TalkBack, to pronounce
256 /// 'h-e-l-l-o-space-w-o-r-l-d' instead of complete words. This is useful for
257 /// texts, such as passwords or verification codes.
258 ///
259 /// If this span contains other text span children, they also inherit the
260 /// property from this span unless explicitly set.
261 ///
262 /// If the property is not set, this text span inherits the spell out setting
263 /// from its parent. If this text span does not have a parent or the parent
264 /// does not have a spell out setting, this text span does not spell out the
265 /// text by default.
266 final bool? spellOut;
267
268 @override
269 bool get validForMouseTracker => true;
270
271 @override
272 void handleEvent(PointerEvent event, HitTestEntry entry) {
273 if (event is PointerDownEvent) {
274 recognizer?.addPointer(event);
275 }
276 }
277
278 /// Apply the [style], [text], and [children] of this object to the
279 /// given [ParagraphBuilder], from which a [Paragraph] can be obtained.
280 /// [Paragraph] objects can be drawn on [Canvas] objects.
281 ///
282 /// Rather than using this directly, it's simpler to use the
283 /// [TextPainter] class to paint [TextSpan] objects onto [Canvas]
284 /// objects.
285 @override
286 void build(
287 ui.ParagraphBuilder builder, {
288 TextScaler textScaler = TextScaler.noScaling,
289 List<PlaceholderDimensions>? dimensions,
290 }) {
291 assert(debugAssertIsValid());
292 final bool hasStyle = style != null;
293 if (hasStyle) {
294 builder.pushStyle(style!.getTextStyle(textScaler: textScaler));
295 }
296 if (text != null) {
297 try {
298 builder.addText(text!);
299 } on ArgumentError catch (exception, stack) {
300 FlutterError.reportError(
301 FlutterErrorDetails(
302 exception: exception,
303 stack: stack,
304 library: 'painting library',
305 context: ErrorDescription('while building a TextSpan'),
306 silent: true,
307 ),
308 );
309 // Use a Unicode replacement character as a substitute for invalid text.
310 builder.addText('\uFFFD');
311 }
312 }
313 final List<InlineSpan>? children = this.children;
314 if (children != null) {
315 for (final InlineSpan child in children) {
316 child.build(builder, textScaler: textScaler, dimensions: dimensions);
317 }
318 }
319 if (hasStyle) {
320 builder.pop();
321 }
322 }
323
324 /// Walks this [TextSpan] and its descendants in pre-order and calls [visitor]
325 /// for each span that has text.
326 ///
327 /// When `visitor` returns true, the walk will continue. When `visitor`
328 /// returns false, then the walk will end.
329 @override
330 bool visitChildren(InlineSpanVisitor visitor) {
331 if (text != null && !visitor(this)) {
332 return false;
333 }
334 final List<InlineSpan>? children = this.children;
335 if (children != null) {
336 for (final InlineSpan child in children) {
337 if (!child.visitChildren(visitor)) {
338 return false;
339 }
340 }
341 }
342 return true;
343 }
344
345 @override
346 bool visitDirectChildren(InlineSpanVisitor visitor) {
347 final List<InlineSpan>? children = this.children;
348 if (children != null) {
349 for (final InlineSpan child in children) {
350 if (!visitor(child)) {
351 return false;
352 }
353 }
354 }
355 return true;
356 }
357
358 /// Returns the text span that contains the given position in the text.
359 @override
360 InlineSpan? getSpanForPositionVisitor(TextPosition position, Accumulator offset) {
361 final String? text = this.text;
362 if (text == null || text.isEmpty) {
363 return null;
364 }
365 final TextAffinity affinity = position.affinity;
366 final int targetOffset = position.offset;
367 final int endOffset = offset.value + text.length;
368
369 if (offset.value == targetOffset && affinity == TextAffinity.downstream ||
370 offset.value < targetOffset && targetOffset < endOffset ||
371 endOffset == targetOffset && affinity == TextAffinity.upstream) {
372 return this;
373 }
374 offset.increment(text.length);
375 return null;
376 }
377
378 @override
379 void computeToPlainText(
380 StringBuffer buffer, {
381 bool includeSemanticsLabels = true,
382 bool includePlaceholders = true,
383 }) {
384 assert(debugAssertIsValid());
385 if (semanticsLabel != null && includeSemanticsLabels) {
386 buffer.write(semanticsLabel);
387 } else if (text != null) {
388 buffer.write(text);
389 }
390 if (children != null) {
391 for (final InlineSpan child in children!) {
392 child.computeToPlainText(
393 buffer,
394 includeSemanticsLabels: includeSemanticsLabels,
395 includePlaceholders: includePlaceholders,
396 );
397 }
398 }
399 }
400
401 @override
402 void computeSemanticsInformation(
403 List<InlineSpanSemanticsInformation> collector, {
404 ui.Locale? inheritedLocale,
405 bool inheritedSpellOut = false,
406 }) {
407 assert(debugAssertIsValid());
408 final ui.Locale? effectiveLocale = locale ?? inheritedLocale;
409 final bool effectiveSpellOut = spellOut ?? inheritedSpellOut;
410
411 if (text != null) {
412 final int textLength = semanticsLabel?.length ?? text!.length;
413 collector.add(
414 InlineSpanSemanticsInformation(
415 text!,
416 stringAttributes: <ui.StringAttribute>[
417 if (effectiveSpellOut && textLength > 0)
418 ui.SpellOutStringAttribute(range: TextRange(start: 0, end: textLength)),
419 if (effectiveLocale != null && textLength > 0)
420 ui.LocaleStringAttribute(
421 locale: effectiveLocale,
422 range: TextRange(start: 0, end: textLength),
423 ),
424 ],
425 semanticsLabel: semanticsLabel,
426 semanticsIdentifier: semanticsIdentifier,
427 recognizer: recognizer,
428 ),
429 );
430 }
431 final List<InlineSpan>? children = this.children;
432 if (children != null) {
433 for (final InlineSpan child in children) {
434 if (child is TextSpan) {
435 child.computeSemanticsInformation(
436 collector,
437 inheritedLocale: effectiveLocale,
438 inheritedSpellOut: effectiveSpellOut,
439 );
440 } else {
441 child.computeSemanticsInformation(collector);
442 }
443 }
444 }
445 }
446
447 @override
448 int? codeUnitAtVisitor(int index, Accumulator offset) {
449 final String? text = this.text;
450 if (text == null) {
451 return null;
452 }
453 final int localOffset = index - offset.value;
454 assert(localOffset >= 0);
455 offset.increment(text.length);
456 return localOffset < text.length ? text.codeUnitAt(localOffset) : null;
457 }
458
459 /// In debug mode, throws an exception if the object is not in a valid
460 /// configuration. Otherwise, returns true.
461 ///
462 /// This is intended to be used as follows:
463 ///
464 /// ```dart
465 /// assert(myTextSpan.debugAssertIsValid());
466 /// ```
467 @override
468 bool debugAssertIsValid() {
469 assert(() {
470 if (children != null) {
471 for (final InlineSpan child in children!) {
472 assert(child.debugAssertIsValid());
473 }
474 }
475 return true;
476 }());
477 return super.debugAssertIsValid();
478 }
479
480 @override
481 RenderComparison compareTo(InlineSpan other) {
482 if (identical(this, other)) {
483 return RenderComparison.identical;
484 }
485 if (other.runtimeType != runtimeType) {
486 return RenderComparison.layout;
487 }
488 final TextSpan textSpan = other as TextSpan;
489 if (textSpan.text != text ||
490 children?.length != textSpan.children?.length ||
491 (style == null) != (textSpan.style == null)) {
492 return RenderComparison.layout;
493 }
494 RenderComparison result = recognizer == textSpan.recognizer
495 ? RenderComparison.identical
496 : RenderComparison.metadata;
497 if (style != null) {
498 final RenderComparison candidate = style!.compareTo(textSpan.style!);
499 if (candidate.index > result.index) {
500 result = candidate;
501 }
502 if (result == RenderComparison.layout) {
503 return result;
504 }
505 }
506 if (children != null) {
507 for (int index = 0; index < children!.length; index += 1) {
508 final RenderComparison candidate = children![index].compareTo(textSpan.children![index]);
509 if (candidate.index > result.index) {
510 result = candidate;
511 }
512 if (result == RenderComparison.layout) {
513 return result;
514 }
515 }
516 }
517 return result;
518 }
519
520 @override
521 bool operator ==(Object other) {
522 if (identical(this, other)) {
523 return true;
524 }
525 if (other.runtimeType != runtimeType) {
526 return false;
527 }
528 if (super != other) {
529 return false;
530 }
531 return other is TextSpan &&
532 other.text == text &&
533 other.recognizer == recognizer &&
534 other.semanticsLabel == semanticsLabel &&
535 other.semanticsIdentifier == semanticsIdentifier &&
536 onEnter == other.onEnter &&
537 onExit == other.onExit &&
538 mouseCursor == other.mouseCursor &&
539 listEquals<InlineSpan>(other.children, children);
540 }
541
542 @override
543 int get hashCode => Object.hash(
544 super.hashCode,
545 text,
546 recognizer,
547 semanticsLabel,
548 semanticsIdentifier,
549 onEnter,
550 onExit,
551 mouseCursor,
552 children == null ? null : Object.hashAll(children!),
553 );
554
555 @override
556 String toStringShort() => objectRuntimeType(this, 'TextSpan');
557
558 @override
559 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
560 super.debugFillProperties(properties);
561
562 properties.add(StringProperty('text', text, showName: false, defaultValue: null));
563 if (style == null && text == null && children == null) {
564 properties.add(DiagnosticsNode.message('(empty)'));
565 }
566
567 properties.add(
568 DiagnosticsProperty<GestureRecognizer>(
569 'recognizer',
570 recognizer,
571 description: recognizer?.runtimeType.toString(),
572 defaultValue: null,
573 ),
574 );
575
576 properties.add(
577 FlagsSummary<Function?>('callbacks', <String, Function?>{'enter': onEnter, 'exit': onExit}),
578 );
579 properties.add(
580 DiagnosticsProperty<MouseCursor>('mouseCursor', cursor, defaultValue: MouseCursor.defer),
581 );
582
583 if (semanticsLabel != null) {
584 properties.add(StringProperty('semanticsLabel', semanticsLabel));
585 }
586
587 if (semanticsIdentifier != null) {
588 properties.add(StringProperty('semanticsIdentifier', semanticsIdentifier));
589 }
590 }
591
592 @override
593 List<DiagnosticsNode> debugDescribeChildren() {
594 return children?.map<DiagnosticsNode>((InlineSpan child) {
595 return child.toDiagnosticsNode();
596 }).toList() ??
597 const <DiagnosticsNode>[];
598 }
599}
600