blob: b2544bd588b2b04bcc731f48b7d2ab3739ca8500 [file] [log] [blame]
// 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 => hashValues(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);
}
}
}