// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui' as ui show ParagraphBuilder, StringAttribute; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'basic_types.dart'; import 'text_painter.dart'; import 'text_span.dart'; import 'text_style.dart'; /// Mutable wrapper of an integer that can be passed by reference to track a /// value across a recursive stack. class Accumulator { /// [Accumulator] may be initialized with a specified value, otherwise, it will /// initialize to zero. Accumulator([this._value = 0]); /// The integer stored in this [Accumulator]. int get value => _value; int _value; /// Increases the [value] by the `addend`. void increment(int addend) { assert(addend >= 0); _value += addend; } } /// Called on each span as [InlineSpan.visitChildren] walks the [InlineSpan] tree. /// /// Returns true when the walk should continue, and false to stop visiting further /// [InlineSpan]s. typedef InlineSpanVisitor = bool Function(InlineSpan span); /// The textual and semantic label information for an [InlineSpan]. /// /// For [PlaceholderSpan]s, [InlineSpanSemanticsInformation.placeholder] is used by default. /// /// See also: /// /// * [InlineSpan.getSemanticsInformation] @immutable class InlineSpanSemanticsInformation { /// Constructs an object that holds the text and semantics label values of an /// [InlineSpan]. /// /// The text parameter must not be null. /// /// Use [InlineSpanSemanticsInformation.placeholder] instead of directly setting /// [isPlaceholder]. const InlineSpanSemanticsInformation( this.text, { this.isPlaceholder = false, this.semanticsLabel, this.stringAttributes = const <ui.StringAttribute>[], this.recognizer, }) : assert(text != null), assert(isPlaceholder != null), assert(isPlaceholder == false || (text == '\uFFFC' && semanticsLabel == null && recognizer == null)), requiresOwnNode = isPlaceholder || recognizer != null; /// The text info for a [PlaceholderSpan]. static const InlineSpanSemanticsInformation placeholder = InlineSpanSemanticsInformation('\uFFFC', isPlaceholder: true); /// The text value, if any. For [PlaceholderSpan]s, this will be the unicode /// placeholder value. final String text; /// The semanticsLabel, if any. final String? semanticsLabel; /// The gesture recognizer, if any, for this span. final GestureRecognizer? recognizer; /// Whether this is for a placeholder span. final bool isPlaceholder; /// True if this configuration should get its own semantics node. /// /// This will be the case of the [recognizer] is not null, of if /// [isPlaceholder] is true. final bool requiresOwnNode; /// The string attributes attached to this semantics information final List<ui.StringAttribute> stringAttributes; @override bool operator ==(Object other) { return other is InlineSpanSemanticsInformation && other.text == text && other.semanticsLabel == semanticsLabel && other.recognizer == recognizer && other.isPlaceholder == isPlaceholder && listEquals<ui.StringAttribute>(other.stringAttributes, stringAttributes); } @override int get hashCode => Object.hash(text, semanticsLabel, recognizer, isPlaceholder); @override String toString() => '${objectRuntimeType(this, 'InlineSpanSemanticsInformation')}{text: $text, semanticsLabel: $semanticsLabel, recognizer: $recognizer}'; } /// Combines _semanticsInfo entries where permissible. /// /// Consecutive inline spans can be combined if their /// [InlineSpanSemanticsInformation.requiresOwnNode] return false. List<InlineSpanSemanticsInformation> combineSemanticsInfo(List<InlineSpanSemanticsInformation> infoList) { final List<InlineSpanSemanticsInformation> combined = <InlineSpanSemanticsInformation>[]; String workingText = ''; String workingLabel = ''; List<ui.StringAttribute> workingAttributes = <ui.StringAttribute>[]; for (final InlineSpanSemanticsInformation info in infoList) { if (info.requiresOwnNode) { combined.add(InlineSpanSemanticsInformation( workingText, semanticsLabel: workingLabel, stringAttributes: workingAttributes, )); workingText = ''; workingLabel = ''; workingAttributes = <ui.StringAttribute>[]; combined.add(info); } else { workingText += info.text; final String effectiveLabel = info.semanticsLabel ?? info.text; for (final ui.StringAttribute infoAttribute in info.stringAttributes) { workingAttributes.add( infoAttribute.copy( range: TextRange( start: infoAttribute.range.start + workingLabel.length, end: infoAttribute.range.end + workingLabel.length, ), ), ); } workingLabel += effectiveLabel; } } combined.add(InlineSpanSemanticsInformation( workingText, semanticsLabel: workingLabel, stringAttributes: workingAttributes, )); return combined; } /// An immutable span of inline content which forms part of a paragraph. /// /// * The subclass [TextSpan] specifies text and may contain child [InlineSpan]s. /// * The subclass [PlaceholderSpan] represents a placeholder that may be /// filled with non-text content. [PlaceholderSpan] itself defines a /// [ui.PlaceholderAlignment] and a [TextBaseline]. To be useful, /// [PlaceholderSpan] must be extended to define content. An instance of /// this is the [WidgetSpan] class in the widgets library. /// * The subclass [WidgetSpan] specifies embedded inline widgets. /// /// {@tool snippet} /// /// This example shows a tree of [InlineSpan]s that make a query asking for a /// name with a [TextField] embedded inline. /// /// ```dart /// Text.rich( /// TextSpan( /// text: 'My name is ', /// style: const TextStyle(color: Colors.black), /// children: <InlineSpan>[ /// WidgetSpan( /// alignment: PlaceholderAlignment.baseline, /// baseline: TextBaseline.alphabetic, /// child: ConstrainedBox( /// constraints: const BoxConstraints(maxWidth: 100), /// child: const TextField(), /// ) /// ), /// const TextSpan( /// text: '.', /// ), /// ], /// ), /// ) /// ``` /// {@end-tool} /// /// See also: /// /// * [Text], a widget for showing uniformly-styled text. /// * [RichText], a widget for finer control of text rendering. /// * [TextPainter], a class for painting [InlineSpan] objects on a [Canvas]. @immutable abstract class InlineSpan extends DiagnosticableTree { /// Creates an [InlineSpan] with the given values. const InlineSpan({ this.style, }); /// The [TextStyle] to apply to this span. /// /// The [style] is also applied to any child spans when this is an instance /// of [TextSpan]. final TextStyle? style; /// Apply the properties of this object to the given [ParagraphBuilder], from /// which a [Paragraph] can be obtained. /// /// The `textScaleFactor` parameter specifies a scale that the text and /// placeholders will be scaled by. The scaling is performed before layout, /// so the text will be laid out with the scaled glyphs and placeholders. /// /// The `dimensions` parameter specifies the sizes of the placeholders. /// Each [PlaceholderSpan] must be paired with a [PlaceholderDimensions] /// in the same order as defined in the [InlineSpan] tree. /// /// [Paragraph] objects can be drawn on [Canvas] objects. void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, List<PlaceholderDimensions>? dimensions }); /// Walks this [InlineSpan] and any descendants in pre-order and calls `visitor` /// for each span that has content. /// /// When `visitor` returns true, the walk will continue. When `visitor` returns /// false, then the walk will end. bool visitChildren(InlineSpanVisitor visitor); /// Returns the [InlineSpan] that contains the given position in the text. InlineSpan? getSpanForPosition(TextPosition position) { assert(debugAssertIsValid()); final Accumulator offset = Accumulator(); InlineSpan? result; visitChildren((InlineSpan span) { result = span.getSpanForPositionVisitor(position, offset); return result == null; }); return result; } /// Performs the check at each [InlineSpan] for if the `position` falls within the range /// of the span and returns the span if it does. /// /// The `offset` parameter tracks the current index offset in the text buffer formed /// if the contents of the [InlineSpan] tree were concatenated together starting /// from the root [InlineSpan]. /// /// This method should not be directly called. Use [getSpanForPosition] instead. @protected InlineSpan? getSpanForPositionVisitor(TextPosition position, Accumulator offset); /// Flattens the [InlineSpan] tree into a single string. /// /// Styles are not honored in this process. If `includeSemanticsLabels` is /// true, then the text returned will include the [TextSpan.semanticsLabel]s /// instead of the text contents for [TextSpan]s. /// /// When `includePlaceholders` is true, [PlaceholderSpan]s in the tree will be /// represented as a 0xFFFC 'object replacement character'. String toPlainText({bool includeSemanticsLabels = true, bool includePlaceholders = true}) { final StringBuffer buffer = StringBuffer(); computeToPlainText(buffer, includeSemanticsLabels: includeSemanticsLabels, includePlaceholders: includePlaceholders); return buffer.toString(); } /// Flattens the [InlineSpan] tree to a list of /// [InlineSpanSemanticsInformation] objects. /// /// [PlaceholderSpan]s in the tree will be represented with a /// [InlineSpanSemanticsInformation.placeholder] value. List<InlineSpanSemanticsInformation> getSemanticsInformation() { final List<InlineSpanSemanticsInformation> collector = <InlineSpanSemanticsInformation>[]; computeSemanticsInformation(collector); return collector; } /// Walks the [InlineSpan] tree and accumulates a list of /// [InlineSpanSemanticsInformation] objects. /// /// This method should not be directly called. Use /// [getSemanticsInformation] instead. /// /// [PlaceholderSpan]s in the tree will be represented with a /// [InlineSpanSemanticsInformation.placeholder] value. @protected void computeSemanticsInformation(List<InlineSpanSemanticsInformation> collector); /// Walks the [InlineSpan] tree and writes the plain text representation to `buffer`. /// /// This method should not be directly called. Use [toPlainText] instead. /// /// Styles are not honored in this process. If `includeSemanticsLabels` is /// true, then the text returned will include the [TextSpan.semanticsLabel]s /// instead of the text contents for [TextSpan]s. /// /// When `includePlaceholders` is true, [PlaceholderSpan]s in the tree will be /// represented as a 0xFFFC 'object replacement character'. /// /// The plain-text representation of this [InlineSpan] is written into the `buffer`. /// This method will then recursively call [computeToPlainText] on its children /// [InlineSpan]s if available. @protected void computeToPlainText(StringBuffer buffer, {bool includeSemanticsLabels = true, bool includePlaceholders = true}); /// Returns the UTF-16 code unit at the given `index` in the flattened string. /// /// This only accounts for the [TextSpan.text] values and ignores [PlaceholderSpan]s. /// /// Returns null if the `index` is out of bounds. int? codeUnitAt(int index) { if (index < 0) { return null; } final Accumulator offset = Accumulator(); int? result; visitChildren((InlineSpan span) { result = span.codeUnitAtVisitor(index, offset); return result == null; }); return result; } /// Performs the check at each [InlineSpan] for if the `index` falls within the range /// of the span and returns the corresponding code unit. Returns null otherwise. /// /// The `offset` parameter tracks the current index offset in the text buffer formed /// if the contents of the [InlineSpan] tree were concatenated together starting /// from the root [InlineSpan]. /// /// This method should not be directly called. Use [codeUnitAt] instead. @protected int? codeUnitAtVisitor(int index, Accumulator offset); /// In debug mode, throws an exception if the object is not in a /// valid configuration. Otherwise, returns true. /// /// This is intended to be used as follows: /// /// ```dart /// assert(myInlineSpan.debugAssertIsValid()); /// ``` bool debugAssertIsValid() => true; /// Describe the difference between this span and another, in terms of /// how much damage it will make to the rendering. The comparison is deep. /// /// Comparing [InlineSpan] objects of different types, for example, comparing /// a [TextSpan] to a [WidgetSpan], always results in [RenderComparison.layout]. /// /// See also: /// /// * [TextStyle.compareTo], which does the same thing for [TextStyle]s. RenderComparison compareTo(InlineSpan other); @override bool operator ==(Object other) { if (identical(this, other)) { return true; } if (other.runtimeType != runtimeType) { return false; } return other is InlineSpan && other.style == style; } @override int get hashCode => style.hashCode; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.whitespace; if (style != null) { style!.debugFillProperties(properties); } } }