blob: d88b9a127cfa61a697a8655f6c15175b098c23fb [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:math' as math;
import 'package:engine/ui.dart' as ui;
import 'package:engine/ui.dart' show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection;
import 'package:collection/collection.dart';
import 'package:flute/foundation.dart';
import 'package:flute/painting.dart' show MatrixUtils, TransformProperty;
import 'package:flute/services.dart';
import 'package:vector_math/vector_math_64.dart';
import 'binding.dart' show SemanticsBinding;
import 'semantics_event.dart';
export 'package:engine/ui.dart' show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection, VoidCallback;
export 'package:flute/foundation.dart' show DiagnosticLevel, DiagnosticPropertiesBuilder, DiagnosticsNode, DiagnosticsTreeStyle, Key, TextTreeConfiguration;
export 'package:flute/services.dart' show TextSelection;
export 'package:vector_math/vector_math_64.dart' show Matrix4;
export 'semantics_event.dart' show SemanticsEvent;
/// Signature for a function that is called for each [SemanticsNode].
///
/// Return false to stop visiting nodes.
///
/// Used by [SemanticsNode.visitChildren].
typedef SemanticsNodeVisitor = bool Function(SemanticsNode node);
/// Signature for [SemanticsAction]s that move the cursor.
///
/// If `extendSelection` is set to true the cursor movement should extend the
/// current selection or (if nothing is currently selected) start a selection.
typedef MoveCursorHandler = void Function(bool extendSelection);
/// Signature for the [SemanticsAction.setSelection] handlers to change the
/// text selection (or re-position the cursor) to `selection`.
typedef SetSelectionHandler = void Function(TextSelection selection);
/// Signature for the [SemanticsAction.setText] handlers to replace the
/// current text with the input `text`.
typedef SetTextHandler = void Function(String text);
/// Signature for a handler of a [SemanticsAction].
///
/// Returned by [SemanticsConfiguration.getActionHandler].
typedef SemanticsActionHandler = void Function(Object? args);
/// Signature for a function that receives a semantics update and returns no result.
///
/// Used by [SemanticsOwner.onSemanticsUpdate].
typedef SemanticsUpdateCallback = void Function(ui.SemanticsUpdate update);
/// Signature for the [SemanticsConfiguration.childConfigurationsDelegate].
///
/// The input list contains all [SemanticsConfiguration]s that rendering
/// children want to merge upward. One can tag a render child with a
/// [SemanticsTag] and look up its [SemanticsConfiguration]s through
/// [SemanticsConfiguration.tagsChildrenWith].
///
/// The return value is the arrangement of these configs, including which
/// configs continue to merge upward and which configs form sibling merge group.
///
/// Use [ChildSemanticsConfigurationsResultBuilder] to generate the return
/// value.
typedef ChildSemanticsConfigurationsDelegate = ChildSemanticsConfigurationsResult Function(List<SemanticsConfiguration>);
/// A tag for a [SemanticsNode].
///
/// Tags can be interpreted by the parent of a [SemanticsNode]
/// and depending on the presence of a tag the parent can for example decide
/// how to add the tagged node as a child. Tags are not sent to the engine.
///
/// As an example, the [RenderSemanticsGestureHandler] uses tags to determine
/// if a child node should be excluded from the scrollable area for semantic
/// purposes.
///
/// The provided [name] is only used for debugging. Two tags created with the
/// same [name] and the `new` operator are not considered identical. However,
/// two tags created with the same [name] and the `const` operator are always
/// identical.
class SemanticsTag {
/// Creates a [SemanticsTag].
///
/// The provided [name] is only used for debugging. Two tags created with the
/// same [name] and the `new` operator are not considered identical. However,
/// two tags created with the same [name] and the `const` operator are always
/// identical.
const SemanticsTag(this.name);
/// A human-readable name for this tag used for debugging.
///
/// This string is not used to determine if two tags are identical.
final String name;
@override
String toString() => '${objectRuntimeType(this, 'SemanticsTag')}($name)';
}
/// The result that contains the arrangement for the child
/// [SemanticsConfiguration]s.
///
/// When the [PipelineOwner] builds the semantics tree, it uses the returned
/// [ChildSemanticsConfigurationsResult] from
/// [SemanticsConfiguration.childConfigurationsDelegate] to decide how semantics nodes
/// should form.
///
/// Use [ChildSemanticsConfigurationsResultBuilder] to build the result.
class ChildSemanticsConfigurationsResult {
ChildSemanticsConfigurationsResult._(this.mergeUp, this.siblingMergeGroups);
/// Returns the [SemanticsConfiguration]s that are supposed to be merged into
/// the parent semantics node.
///
/// [SemanticsConfiguration]s that are either semantics boundaries or are
/// conflicting with other [SemanticsConfiguration]s will form explicit
/// semantics nodes. All others will be merged into the parent.
final List<SemanticsConfiguration> mergeUp;
/// The groups of child semantics configurations that want to merge together
/// and form a sibling [SemanticsNode].
///
/// All the [SemanticsConfiguration]s in a given group that are either
/// semantics boundaries or are conflicting with other
/// [SemanticsConfiguration]s of the same group will be excluded from the
/// sibling merge group and form independent semantics nodes as usual.
///
/// The result [SemanticsNode]s from the merges are attached as the sibling
/// nodes of the immediate parent semantics node. For example, a `RenderObjectA`
/// has a rendering child, `RenderObjectB`. If both of them form their own
/// semantics nodes, `SemanticsNodeA` and `SemanticsNodeB`, any semantics node
/// created from sibling merge groups of `RenderObjectB` will be attach to
/// `SemanticsNodeA` as a sibling of `SemanticsNodeB`.
final List<List<SemanticsConfiguration>> siblingMergeGroups;
}
/// The builder to build a [ChildSemanticsConfigurationsResult] based on its
/// annotations.
///
/// To use this builder, one can use [markAsMergeUp] and
/// [markAsSiblingMergeGroup] to annotate the arrangement of
/// [SemanticsConfiguration]s. Once all the configs are annotated, use [build]
/// to generate the [ChildSemanticsConfigurationsResult].
class ChildSemanticsConfigurationsResultBuilder {
/// Creates a [ChildSemanticsConfigurationsResultBuilder].
ChildSemanticsConfigurationsResultBuilder();
final List<SemanticsConfiguration> _mergeUp = <SemanticsConfiguration>[];
final List<List<SemanticsConfiguration>> _siblingMergeGroups = <List<SemanticsConfiguration>>[];
/// Marks the [SemanticsConfiguration] to be merged into the parent semantics
/// node.
///
/// The [SemanticsConfiguration] will be added to the
/// [ChildSemanticsConfigurationsResult.mergeUp] that this builder builds.
void markAsMergeUp(SemanticsConfiguration config) => _mergeUp.add(config);
/// Marks a group of [SemanticsConfiguration]s to merge together
/// and form a sibling [SemanticsNode].
///
/// The group of [SemanticsConfiguration]s will be added to the
/// [ChildSemanticsConfigurationsResult.siblingMergeGroups] that this builder builds.
void markAsSiblingMergeGroup(List<SemanticsConfiguration> configs) => _siblingMergeGroups.add(configs);
/// Builds a [ChildSemanticsConfigurationsResult] contains the arrangement.
ChildSemanticsConfigurationsResult build() {
assert((){
final Set<SemanticsConfiguration> seenConfigs = <SemanticsConfiguration>{};
for (final SemanticsConfiguration config in <SemanticsConfiguration>[..._mergeUp, ..._siblingMergeGroups.flattened]) {
assert(
seenConfigs.add(config),
'Duplicated SemanticsConfigurations. This can happen if the same '
'SemanticsConfiguration was marked twice in markAsMergeUp and/or '
'markAsSiblingMergeGroup'
);
}
return true;
}());
return ChildSemanticsConfigurationsResult._(_mergeUp, _siblingMergeGroups);
}
}
/// An identifier of a custom semantics action.
///
/// Custom semantics actions can be provided to make complex user
/// interactions more accessible. For instance, if an application has a
/// drag-and-drop list that requires the user to press and hold an item
/// to move it, users interacting with the application using a hardware
/// switch may have difficulty. This can be made accessible by creating custom
/// actions and pairing them with handlers that move a list item up or down in
/// the list.
///
/// In Android, these actions are presented in the local context menu. In iOS,
/// these are presented in the radial context menu.
///
/// Localization and text direction do not automatically apply to the provided
/// label or hint.
///
/// Instances of this class should either be instantiated with const or
/// new instances cached in static fields.
///
/// See also:
///
/// * [SemanticsProperties], where the handler for a custom action is provided.
@immutable
class CustomSemanticsAction {
/// Creates a new [CustomSemanticsAction].
///
/// The [label] must not be null or the empty string.
const CustomSemanticsAction({required String this.label})
: assert(label != ''),
hint = null,
action = null;
/// Creates a new [CustomSemanticsAction] that overrides a standard semantics
/// action.
///
/// The [hint] must not be null or the empty string.
const CustomSemanticsAction.overridingAction({required String this.hint, required SemanticsAction this.action})
: assert(hint != ''),
label = null;
/// The user readable name of this custom semantics action.
final String? label;
/// The hint description of this custom semantics action.
final String? hint;
/// The standard semantics action this action replaces.
final SemanticsAction? action;
@override
int get hashCode => Object.hash(label, hint, action);
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is CustomSemanticsAction
&& other.label == label
&& other.hint == hint
&& other.action == action;
}
@override
String toString() {
return 'CustomSemanticsAction(${_ids[this]}, label:$label, hint:$hint, action:$action)';
}
// Logic to assign a unique id to each custom action without requiring
// user specification.
static int _nextId = 0;
static final Map<int, CustomSemanticsAction> _actions = <int, CustomSemanticsAction>{};
static final Map<CustomSemanticsAction, int> _ids = <CustomSemanticsAction, int>{};
/// Get the identifier for a given `action`.
static int getIdentifier(CustomSemanticsAction action) {
int? result = _ids[action];
if (result == null) {
result = _nextId++;
_ids[action] = result;
_actions[result] = action;
}
return result;
}
/// Get the `action` for a given identifier.
static CustomSemanticsAction? getAction(int id) {
return _actions[id];
}
}
/// A string that carries a list of [StringAttribute]s.
@immutable
class AttributedString {
/// Creates a attributed string.
///
/// The [TextRange] in the [attributes] must be inside the length of the
/// [string].
///
/// The [attributes] must not be changed after the attributed string is
/// created.
AttributedString(
this.string, {
this.attributes = const <StringAttribute>[],
}) : assert(string.isNotEmpty || attributes.isEmpty),
assert(() {
for (final StringAttribute attribute in attributes) {
assert(
string.length >= attribute.range.start &&
string.length >= attribute.range.end,
'The range in $attribute is outside of the string $string',
);
}
return true;
}());
/// The plain string stored in the attributed string.
final String string;
/// The attributes this string carries.
///
/// The list must not be modified after this string is created.
final List<StringAttribute> attributes;
/// Returns a new [AttributedString] by concatenate the operands
///
/// The string attribute list of the returned [AttributedString] will contains
/// the string attributes from both operands with updated text ranges.
AttributedString operator +(AttributedString other) {
if (string.isEmpty) {
return other;
}
if (other.string.isEmpty) {
return this;
}
// None of the strings is empty.
final String newString = string + other.string;
final List<StringAttribute> newAttributes = List<StringAttribute>.of(attributes);
if (other.attributes.isNotEmpty) {
final int offset = string.length;
for (final StringAttribute attribute in other.attributes) {
final TextRange newRange = TextRange(
start: attribute.range.start + offset,
end: attribute.range.end + offset,
);
final StringAttribute adjustedAttribute = attribute.copy(range: newRange);
newAttributes.add(adjustedAttribute);
}
}
return AttributedString(newString, attributes: newAttributes);
}
/// Two [AttributedString]s are equal if their string and attributes are.
@override
bool operator ==(Object other) {
return other.runtimeType == runtimeType
&& other is AttributedString
&& other.string == string
&& listEquals<StringAttribute>(other.attributes, attributes);
}
@override
int get hashCode => Object.hash(string, attributes);
@override
String toString() {
return "${objectRuntimeType(this, 'AttributedString')}('$string', attributes: $attributes)";
}
}
/// A [DiagnosticsProperty] for [AttributedString]s, which shows a string
/// when there are no attributes, and more details otherwise.
class AttributedStringProperty extends DiagnosticsProperty<AttributedString> {
/// Create a diagnostics property for an [AttributedString] object.
///
/// Such properties are used with [SemanticsData] objects.
AttributedStringProperty(
String super.name,
super.value, {
super.showName,
this.showWhenEmpty = false,
super.defaultValue,
super.level,
super.description,
});
/// Whether to show the property when the [value] is an [AttributedString]
/// whose [AttributedString.string] is the empty string.
///
/// This overrides [defaultValue].
final bool showWhenEmpty;
@override
bool get isInteresting => super.isInteresting && (showWhenEmpty || (value != null && value!.string.isNotEmpty));
@override
String valueToString({TextTreeConfiguration? parentConfiguration}) {
if (value == null) {
return 'null';
}
String text = value!.string;
if (parentConfiguration != null &&
!parentConfiguration.lineBreakProperties) {
// This follows a similar pattern to StringProperty.
text = text.replaceAll('\n', r'\n');
}
if (value!.attributes.isEmpty) {
return '"$text"';
}
return '"$text" ${value!.attributes}'; // the attributes will be in square brackets since they're a list
}
}
/// Summary information about a [SemanticsNode] object.
///
/// A semantics node might [SemanticsNode.mergeAllDescendantsIntoThisNode],
/// which means the individual fields on the semantics node don't fully describe
/// the semantics at that node. This data structure contains the full semantics
/// for the node.
///
/// Typically obtained from [SemanticsNode.getSemanticsData].
@immutable
class SemanticsData with Diagnosticable {
/// Creates a semantics data object.
///
/// The [flags], [actions], [label], and [Rect] arguments must not be null.
///
/// If [label] is not empty, then [textDirection] must also not be null.
SemanticsData({
required this.flags,
required this.actions,
required this.attributedLabel,
required this.attributedValue,
required this.attributedIncreasedValue,
required this.attributedDecreasedValue,
required this.attributedHint,
required this.tooltip,
required this.textDirection,
required this.rect,
required this.elevation,
required this.thickness,
required this.textSelection,
required this.scrollIndex,
required this.scrollChildCount,
required this.scrollPosition,
required this.scrollExtentMax,
required this.scrollExtentMin,
required this.platformViewId,
required this.maxValueLength,
required this.currentValueLength,
this.tags,
this.transform,
this.customSemanticsActionIds,
}) : assert(tooltip == '' || textDirection != null, 'A SemanticsData object with tooltip "$tooltip" had a null textDirection.'),
assert(attributedLabel.string == '' || textDirection != null, 'A SemanticsData object with label "${attributedLabel.string}" had a null textDirection.'),
assert(attributedValue.string == '' || textDirection != null, 'A SemanticsData object with value "${attributedValue.string}" had a null textDirection.'),
assert(attributedDecreasedValue.string == '' || textDirection != null, 'A SemanticsData object with decreasedValue "${attributedDecreasedValue.string}" had a null textDirection.'),
assert(attributedIncreasedValue.string == '' || textDirection != null, 'A SemanticsData object with increasedValue "${attributedIncreasedValue.string}" had a null textDirection.'),
assert(attributedHint.string == '' || textDirection != null, 'A SemanticsData object with hint "${attributedHint.string}" had a null textDirection.');
/// A bit field of [SemanticsFlag]s that apply to this node.
final int flags;
/// A bit field of [SemanticsAction]s that apply to this node.
final int actions;
/// A textual description for the current label of the node.
///
/// The reading direction is given by [textDirection].
///
/// This exposes the raw text of the [attributedLabel].
String get label => attributedLabel.string;
/// A textual description for the current label of the node in
/// [AttributedString] format.
///
/// The reading direction is given by [textDirection].
///
/// See also [label], which exposes just the raw text.
final AttributedString attributedLabel;
/// A textual description for the current value of the node.
///
/// The reading direction is given by [textDirection].
///
/// This exposes the raw text of the [attributedValue].
String get value => attributedValue.string;
/// A textual description for the current value of the node in
/// [AttributedString] format.
///
/// The reading direction is given by [textDirection].
///
/// See also [value], which exposes just the raw text.
final AttributedString attributedValue;
/// The value that [value] will become after performing a
/// [SemanticsAction.increase] action.
///
/// The reading direction is given by [textDirection].
///
/// This exposes the raw text of the [attributedIncreasedValue].
String get increasedValue => attributedIncreasedValue.string;
/// The value that [value] will become after performing a
/// [SemanticsAction.increase] action in [AttributedString] format.
///
/// The reading direction is given by [textDirection].
///
/// See also [increasedValue], which exposes just the raw text.
final AttributedString attributedIncreasedValue;
/// The value that [value] will become after performing a
/// [SemanticsAction.decrease] action.
///
/// The reading direction is given by [textDirection].
///
/// This exposes the raw text of the [attributedDecreasedValue].
String get decreasedValue => attributedDecreasedValue.string;
/// The value that [value] will become after performing a
/// [SemanticsAction.decrease] action in [AttributedString] format.
///
/// The reading direction is given by [textDirection].
///
/// See also [decreasedValue], which exposes just the raw text.
final AttributedString attributedDecreasedValue;
/// A brief description of the result of performing an action on this node.
///
/// The reading direction is given by [textDirection].
///
/// This exposes the raw text of the [attributedHint].
String get hint => attributedHint.string;
/// A brief description of the result of performing an action on this node
/// in [AttributedString] format.
///
/// The reading direction is given by [textDirection].
///
/// See also [hint], which exposes just the raw text.
final AttributedString attributedHint;
/// A textual description of the widget's tooltip.
///
/// The reading direction is given by [textDirection].
final String tooltip;
/// The reading direction for the text in [label], [value],
/// [increasedValue], [decreasedValue], and [hint].
final TextDirection? textDirection;
/// The currently selected text (or the position of the cursor) within [value]
/// if this node represents a text field.
final TextSelection? textSelection;
/// The total number of scrollable children that contribute to semantics.
///
/// If the number of children are unknown or unbounded, this value will be
/// null.
final int? scrollChildCount;
/// The index of the first visible semantic child of a scroll node.
final int? scrollIndex;
/// Indicates the current scrolling position in logical pixels if the node is
/// scrollable.
///
/// The properties [scrollExtentMin] and [scrollExtentMax] indicate the valid
/// in-range values for this property. The value for [scrollPosition] may
/// (temporarily) be outside that range, e.g. during an overscroll.
///
/// See also:
///
/// * [ScrollPosition.pixels], from where this value is usually taken.
final double? scrollPosition;
/// Indicates the maximum in-range value for [scrollPosition] if the node is
/// scrollable.
///
/// This value may be infinity if the scroll is unbound.
///
/// See also:
///
/// * [ScrollPosition.maxScrollExtent], from where this value is usually taken.
final double? scrollExtentMax;
/// Indicates the minimum in-range value for [scrollPosition] if the node is
/// scrollable.
///
/// This value may be infinity if the scroll is unbound.
///
/// See also:
///
/// * [ScrollPosition.minScrollExtent], from where this value is usually taken.
final double? scrollExtentMin;
/// The id of the platform view, whose semantics nodes will be added as
/// children to this node.
///
/// If this value is non-null, the SemanticsNode must not have any children
/// as those would be replaced by the semantics nodes of the referenced
/// platform view.
///
/// See also:
///
/// * [AndroidView], which is the platform view for Android.
/// * [UiKitView], which is the platform view for iOS.
final int? platformViewId;
/// The maximum number of characters that can be entered into an editable
/// text field.
///
/// For the purpose of this function a character is defined as one Unicode
/// scalar value.
///
/// This should only be set when [SemanticsFlag.isTextField] is set. Defaults
/// to null, which means no limit is imposed on the text field.
final int? maxValueLength;
/// The current number of characters that have been entered into an editable
/// text field.
///
/// For the purpose of this function a character is defined as one Unicode
/// scalar value.
///
/// This should only be set when [SemanticsFlag.isTextField] is set. This must
/// be set when [maxValueLength] is set.
final int? currentValueLength;
/// The bounding box for this node in its coordinate system.
final Rect rect;
/// The set of [SemanticsTag]s associated with this node.
final Set<SemanticsTag>? tags;
/// The transform from this node's coordinate system to its parent's coordinate system.
///
/// By default, the transform is null, which represents the identity
/// transformation (i.e., that this node has the same coordinate system as its
/// parent).
final Matrix4? transform;
/// The elevation of this node relative to the parent semantics node.
///
/// See also:
///
/// * [SemanticsConfiguration.elevation] for a detailed discussion regarding
/// elevation and semantics.
final double elevation;
/// The extent of this node along the z-axis beyond its [elevation]
///
/// See also:
///
/// * [SemanticsConfiguration.thickness] for a more detailed definition.
final double thickness;
/// The identifiers for the custom semantics actions and standard action
/// overrides for this node.
///
/// The list must be sorted in increasing order.
///
/// See also:
///
/// * [CustomSemanticsAction], for an explanation of custom actions.
final List<int>? customSemanticsActionIds;
/// Whether [flags] contains the given flag.
bool hasFlag(SemanticsFlag flag) => (flags & flag.index) != 0;
/// Whether [actions] contains the given action.
bool hasAction(SemanticsAction action) => (actions & action.index) != 0;
@override
String toStringShort() => objectRuntimeType(this, 'SemanticsData');
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Rect>('rect', rect, showName: false));
properties.add(TransformProperty('transform', transform, showName: false, defaultValue: null));
properties.add(DoubleProperty('elevation', elevation, defaultValue: 0.0));
properties.add(DoubleProperty('thickness', thickness, defaultValue: 0.0));
final List<String> actionSummary = <String>[
for (final SemanticsAction action in SemanticsAction.values.values)
if ((actions & action.index) != 0)
describeEnum(action),
];
final List<String?> customSemanticsActionSummary = customSemanticsActionIds!
.map<String?>((int actionId) => CustomSemanticsAction.getAction(actionId)!.label)
.toList();
properties.add(IterableProperty<String>('actions', actionSummary, ifEmpty: null));
properties.add(IterableProperty<String?>('customActions', customSemanticsActionSummary, ifEmpty: null));
final List<String> flagSummary = <String>[
for (final SemanticsFlag flag in SemanticsFlag.values.values)
if ((flags & flag.index) != 0)
describeEnum(flag),
];
properties.add(IterableProperty<String>('flags', flagSummary, ifEmpty: null));
properties.add(AttributedStringProperty('label', attributedLabel));
properties.add(AttributedStringProperty('value', attributedValue));
properties.add(AttributedStringProperty('increasedValue', attributedIncreasedValue));
properties.add(AttributedStringProperty('decreasedValue', attributedDecreasedValue));
properties.add(AttributedStringProperty('hint', attributedHint));
properties.add(StringProperty('tooltip', tooltip, defaultValue: ''));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
if (textSelection?.isValid ?? false) {
properties.add(MessageProperty('textSelection', '[${textSelection!.start}, ${textSelection!.end}]'));
}
properties.add(IntProperty('platformViewId', platformViewId, defaultValue: null));
properties.add(IntProperty('maxValueLength', maxValueLength, defaultValue: null));
properties.add(IntProperty('currentValueLength', currentValueLength, defaultValue: null));
properties.add(IntProperty('scrollChildren', scrollChildCount, defaultValue: null));
properties.add(IntProperty('scrollIndex', scrollIndex, defaultValue: null));
properties.add(DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null));
properties.add(DoubleProperty('scrollPosition', scrollPosition, defaultValue: null));
properties.add(DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null));
}
@override
bool operator ==(Object other) {
return other is SemanticsData
&& other.flags == flags
&& other.actions == actions
&& other.attributedLabel == attributedLabel
&& other.attributedValue == attributedValue
&& other.attributedIncreasedValue == attributedIncreasedValue
&& other.attributedDecreasedValue == attributedDecreasedValue
&& other.attributedHint == attributedHint
&& other.tooltip == tooltip
&& other.textDirection == textDirection
&& other.rect == rect
&& setEquals(other.tags, tags)
&& other.scrollChildCount == scrollChildCount
&& other.scrollIndex == scrollIndex
&& other.textSelection == textSelection
&& other.scrollPosition == scrollPosition
&& other.scrollExtentMax == scrollExtentMax
&& other.scrollExtentMin == scrollExtentMin
&& other.platformViewId == platformViewId
&& other.maxValueLength == maxValueLength
&& other.currentValueLength == currentValueLength
&& other.transform == transform
&& other.elevation == elevation
&& other.thickness == thickness
&& _sortedListsEqual(other.customSemanticsActionIds, customSemanticsActionIds);
}
@override
int get hashCode => Object.hash(
flags,
actions,
attributedLabel,
attributedValue,
attributedIncreasedValue,
attributedDecreasedValue,
attributedHint,
tooltip,
textDirection,
rect,
tags,
textSelection,
scrollChildCount,
scrollIndex,
scrollPosition,
scrollExtentMax,
scrollExtentMin,
platformViewId,
maxValueLength,
Object.hash(
currentValueLength,
transform,
elevation,
thickness,
customSemanticsActionIds == null ? null : Object.hashAll(customSemanticsActionIds!),
),
);
static bool _sortedListsEqual(List<int>? left, List<int>? right) {
if (left == null && right == null) {
return true;
}
if (left != null && right != null) {
if (left.length != right.length) {
return false;
}
for (int i = 0; i < left.length; i++) {
if (left[i] != right[i]) {
return false;
}
}
return true;
}
return false;
}
}
class _SemanticsDiagnosticableNode extends DiagnosticableNode<SemanticsNode> {
_SemanticsDiagnosticableNode({
super.name,
required super.value,
required super.style,
required this.childOrder,
});
final DebugSemanticsDumpOrder childOrder;
@override
List<DiagnosticsNode> getChildren() => value.debugDescribeChildren(childOrder: childOrder);
}
/// Provides hint values which override the default hints on supported
/// platforms.
///
/// On iOS, these values are always ignored.
@immutable
class SemanticsHintOverrides extends DiagnosticableTree {
/// Creates a semantics hint overrides.
const SemanticsHintOverrides({
this.onTapHint,
this.onLongPressHint,
}) : assert(onTapHint != ''),
assert(onLongPressHint != '');
/// The hint text for a tap action.
///
/// If null, the standard hint is used instead.
///
/// The hint should describe what happens when a tap occurs, not the
/// manner in which a tap is accomplished.
///
/// Bad: 'Double tap to show movies'.
/// Good: 'show movies'.
final String? onTapHint;
/// The hint text for a long press action.
///
/// If null, the standard hint is used instead.
///
/// The hint should describe what happens when a long press occurs, not
/// the manner in which the long press is accomplished.
///
/// Bad: 'Double tap and hold to show tooltip'.
/// Good: 'show tooltip'.
final String? onLongPressHint;
/// Whether there are any non-null hint values.
bool get isNotEmpty => onTapHint != null || onLongPressHint != null;
@override
int get hashCode => Object.hash(onTapHint, onLongPressHint);
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is SemanticsHintOverrides
&& other.onTapHint == onTapHint
&& other.onLongPressHint == onLongPressHint;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('onTapHint', onTapHint, defaultValue: null));
properties.add(StringProperty('onLongPressHint', onLongPressHint, defaultValue: null));
}
}
/// Contains properties used by assistive technologies to make the application
/// more accessible.
///
/// The properties of this class are used to generate a [SemanticsNode]s in the
/// semantics tree.
@immutable
class SemanticsProperties extends DiagnosticableTree {
/// Creates a semantic annotation.
const SemanticsProperties({
this.enabled,
this.checked,
this.mixed,
this.selected,
this.toggled,
this.button,
this.link,
this.header,
this.textField,
this.slider,
this.keyboardKey,
this.readOnly,
this.focusable,
this.focused,
this.inMutuallyExclusiveGroup,
this.hidden,
this.obscured,
this.multiline,
this.scopesRoute,
this.namesRoute,
this.image,
this.liveRegion,
this.maxValueLength,
this.currentValueLength,
this.label,
this.attributedLabel,
this.value,
this.attributedValue,
this.increasedValue,
this.attributedIncreasedValue,
this.decreasedValue,
this.attributedDecreasedValue,
this.hint,
this.tooltip,
this.attributedHint,
this.hintOverrides,
this.textDirection,
this.sortKey,
this.tagForChildren,
this.onTap,
this.onLongPress,
this.onScrollLeft,
this.onScrollRight,
this.onScrollUp,
this.onScrollDown,
this.onIncrease,
this.onDecrease,
this.onCopy,
this.onCut,
this.onPaste,
this.onMoveCursorForwardByCharacter,
this.onMoveCursorBackwardByCharacter,
this.onMoveCursorForwardByWord,
this.onMoveCursorBackwardByWord,
this.onSetSelection,
this.onSetText,
this.onDidGainAccessibilityFocus,
this.onDidLoseAccessibilityFocus,
this.onDismiss,
this.customSemanticsActions,
}) : assert(label == null || attributedLabel == null, 'Only one of label or attributedLabel should be provided'),
assert(value == null || attributedValue == null, 'Only one of value or attributedValue should be provided'),
assert(increasedValue == null || attributedIncreasedValue == null, 'Only one of increasedValue or attributedIncreasedValue should be provided'),
assert(decreasedValue == null || attributedDecreasedValue == null, 'Only one of decreasedValue or attributedDecreasedValue should be provided'),
assert(hint == null || attributedHint == null, 'Only one of hint or attributedHint should be provided');
/// If non-null, indicates that this subtree represents something that can be
/// in an enabled or disabled state.
///
/// For example, a button that a user can currently interact with would set
/// this field to true. A button that currently does not respond to user
/// interactions would set this field to false.
final bool? enabled;
/// If non-null, indicates that this subtree represents a checkbox
/// or similar widget with a "checked" state, and what its current
/// state is.
///
/// When the [Checkbox.value] of a tristate Checkbox is null,
/// indicating a mixed-state, this value shall be false, in which
/// case, [mixed] will be true.
///
/// This is mutually exclusive with [toggled] and [mixed].
final bool? checked;
/// If non-null, indicates that this subtree represents a checkbox
/// or similar widget with a "half-checked" state or similar, and
/// whether it is currently in this half-checked state.
///
/// This must be null when [Checkbox.tristate] is false, or
/// when the widget is not a checkbox. When a tristate
/// checkbox is fully unchecked/checked, this value shall
/// be false.
///
/// This is mutually exclusive with [checked] and [toggled].
final bool? mixed;
/// If non-null, indicates that this subtree represents a toggle switch
/// or similar widget with an "on" state, and what its current
/// state is.
///
/// This is mutually exclusive with [checked] and [mixed].
final bool? toggled;
/// If non-null indicates that this subtree represents something that can be
/// in a selected or unselected state, and what its current state is.
///
/// The active tab in a tab bar for example is considered "selected", whereas
/// all other tabs are unselected.
final bool? selected;
/// If non-null, indicates that this subtree represents a button.
///
/// TalkBack/VoiceOver provides users with the hint "button" when a button
/// is focused.
final bool? button;
/// If non-null, indicates that this subtree represents a link.
///
/// iOS's VoiceOver provides users with a unique hint when a link is focused.
/// Android's Talkback will announce a link hint the same way it does a
/// button.
final bool? link;
/// If non-null, indicates that this subtree represents a header.
///
/// A header divides into sections. For example, an address book application
/// might define headers A, B, C, etc. to divide the list of alphabetically
/// sorted contacts into sections.
final bool? header;
/// If non-null, indicates that this subtree represents a text field.
///
/// TalkBack/VoiceOver provide special affordances to enter text into a
/// text field.
final bool? textField;
/// If non-null, indicates that this subtree represents a slider.
///
/// Talkback/\VoiceOver provides users with the hint "slider" when a
/// slider is focused.
final bool? slider;
/// If non-null, indicates that this subtree represents a keyboard key.
final bool? keyboardKey;
/// If non-null, indicates that this subtree is read only.
///
/// Only applicable when [textField] is true.
///
/// TalkBack/VoiceOver will treat it as non-editable text field.
final bool? readOnly;
/// If non-null, whether the node is able to hold input focus.
///
/// If [focusable] is set to false, then [focused] must not be true.
///
/// Input focus indicates that the node will receive keyboard events. It is not
/// to be confused with accessibility focus. Accessibility focus is the
/// green/black rectangular highlight that TalkBack/VoiceOver draws around the
/// element it is reading, and is separate from input focus.
final bool? focusable;
/// If non-null, whether the node currently holds input focus.
///
/// At most one node in the tree should hold input focus at any point in time,
/// and it should not be set to true if [focusable] is false.
///
/// Input focus indicates that the node will receive keyboard events. It is not
/// to be confused with accessibility focus. Accessibility focus is the
/// green/black rectangular highlight that TalkBack/VoiceOver draws around the
/// element it is reading, and is separate from input focus.
final bool? focused;
/// If non-null, whether a semantic node is in a mutually exclusive group.
///
/// For example, a radio button is in a mutually exclusive group because only
/// one radio button in that group can be marked as [checked].
final bool? inMutuallyExclusiveGroup;
/// If non-null, whether the node is considered hidden.
///
/// Hidden elements are currently not visible on screen. They may be covered
/// by other elements or positioned outside of the visible area of a viewport.
///
/// Hidden elements cannot gain accessibility focus though regular touch. The
/// only way they can be focused is by moving the focus to them via linear
/// navigation.
///
/// Platforms are free to completely ignore hidden elements and new platforms
/// are encouraged to do so.
///
/// Instead of marking an element as hidden it should usually be excluded from
/// the semantics tree altogether. Hidden elements are only included in the
/// semantics tree to work around platform limitations and they are mainly
/// used to implement accessibility scrolling on iOS.
final bool? hidden;
/// If non-null, whether [value] should be obscured.
///
/// This option is usually set in combination with [textField] to indicate
/// that the text field contains a password (or other sensitive information).
/// Doing so instructs screen readers to not read out the [value].
final bool? obscured;
/// Whether the [value] is coming from a field that supports multiline text
/// editing.
///
/// This option is only meaningful when [textField] is true to indicate
/// whether it's a single-line or multiline text field.
///
/// This option is null when [textField] is false.
final bool? multiline;
/// If non-null, whether the node corresponds to the root of a subtree for
/// which a route name should be announced.
///
/// Generally, this is set in combination with
/// [SemanticsConfiguration.explicitChildNodes], since nodes with this flag
/// are not considered focusable by Android or iOS.
///
/// See also:
///
/// * [SemanticsFlag.scopesRoute] for a description of how the announced
/// value is selected.
final bool? scopesRoute;
/// If non-null, whether the node contains the semantic label for a route.
///
/// See also:
///
/// * [SemanticsFlag.namesRoute] for a description of how the name is used.
final bool? namesRoute;
/// If non-null, whether the node represents an image.
///
/// See also:
///
/// * [SemanticsFlag.isImage], for the flag this setting controls.
final bool? image;
/// If non-null, whether the node should be considered a live region.
///
/// A live region indicates that updates to semantics node are important.
/// Platforms may use this information to make polite announcements to the
/// user to inform them of updates to this node.
///
/// An example of a live region is a [SnackBar] widget. On Android and iOS,
/// live region causes a polite announcement to be generated automatically,
/// even if the widget does not have accessibility focus. This announcement
/// may not be spoken if the OS accessibility services are already
/// announcing something else, such as reading the label of a focused widget
/// or providing a system announcement.
///
/// See also:
///
/// * [SemanticsFlag.isLiveRegion], the semantics flag this setting controls.
/// * [SemanticsConfiguration.liveRegion], for a full description of a live region.
final bool? liveRegion;
/// The maximum number of characters that can be entered into an editable
/// text field.
///
/// For the purpose of this function a character is defined as one Unicode
/// scalar value.
///
/// This should only be set when [textField] is true. Defaults to null,
/// which means no limit is imposed on the text field.
final int? maxValueLength;
/// The current number of characters that have been entered into an editable
/// text field.
///
/// For the purpose of this function a character is defined as one Unicode
/// scalar value.
///
/// This should only be set when [textField] is true. Must be set when
/// [maxValueLength] is set.
final int? currentValueLength;
/// Provides a textual description of the widget.
///
/// If a label is provided, there must either by an ambient [Directionality]
/// or an explicit [textDirection] should be provided.
///
/// Callers must not provide both [label] and [attributedLabel]. One or both
/// must be null.
///
/// See also:
///
/// * [SemanticsConfiguration.label] for a description of how this is exposed
/// in TalkBack and VoiceOver.
/// * [attributedLabel] for an [AttributedString] version of this property.
final String? label;
/// Provides an [AttributedString] version of textual description of the widget.
///
/// If a [attributedLabel] is provided, there must either by an ambient
/// [Directionality] or an explicit [textDirection] should be provided.
///
/// Callers must not provide both [label] and [attributedLabel]. One or both
/// must be null.
///
/// See also:
///
/// * [SemanticsConfiguration.attributedLabel] for a description of how this
/// is exposed in TalkBack and VoiceOver.
/// * [label] for a plain string version of this property.
final AttributedString? attributedLabel;
/// Provides a textual description of the value of the widget.
///
/// If a value is provided, there must either by an ambient [Directionality]
/// or an explicit [textDirection] should be provided.
///
/// Callers must not provide both [value] and [attributedValue], One or both
/// must be null.
///
/// See also:
///
/// * [SemanticsConfiguration.value] for a description of how this is exposed
/// in TalkBack and VoiceOver.
/// * [attributedLabel] for an [AttributedString] version of this property.
final String? value;
/// Provides an [AttributedString] version of textual description of the value
/// of the widget.
///
/// If a [attributedValue] is provided, there must either by an ambient
/// [Directionality] or an explicit [textDirection] should be provided.
///
/// Callers must not provide both [value] and [attributedValue], One or both
/// must be null.
///
/// See also:
///
/// * [SemanticsConfiguration.attributedValue] for a description of how this
/// is exposed in TalkBack and VoiceOver.
/// * [value] for a plain string version of this property.
final AttributedString? attributedValue;
/// The value that [value] or [attributedValue] will become after a
/// [SemanticsAction.increase] action has been performed on this widget.
///
/// If a value is provided, [onIncrease] must also be set and there must
/// either be an ambient [Directionality] or an explicit [textDirection]
/// must be provided.
///
/// Callers must not provide both [increasedValue] and
/// [attributedIncreasedValue], One or both must be null.
///
/// See also:
///
/// * [SemanticsConfiguration.increasedValue] for a description of how this
/// is exposed in TalkBack and VoiceOver.
/// * [attributedIncreasedValue] for an [AttributedString] version of this
/// property.
final String? increasedValue;
/// The [AttributedString] that [value] or [attributedValue] will become after
/// a [SemanticsAction.increase] action has been performed on this widget.
///
/// If a [attributedIncreasedValue] is provided, [onIncrease] must also be set
/// and there must either be an ambient [Directionality] or an explicit
/// [textDirection] must be provided.
///
/// Callers must not provide both [increasedValue] and
/// [attributedIncreasedValue], One or both must be null.
///
/// See also:
///
/// * [SemanticsConfiguration.attributedIncreasedValue] for a description of
/// how this is exposed in TalkBack and VoiceOver.
/// * [increasedValue] for a plain string version of this property.
final AttributedString? attributedIncreasedValue;
/// The value that [value] or [attributedValue] will become after a
/// [SemanticsAction.decrease] action has been performed on this widget.
///
/// If a value is provided, [onDecrease] must also be set and there must
/// either be an ambient [Directionality] or an explicit [textDirection]
/// must be provided.
///
/// Callers must not provide both [decreasedValue] and
/// [attributedDecreasedValue], One or both must be null.
///
/// See also:
///
/// * [SemanticsConfiguration.decreasedValue] for a description of how this
/// is exposed in TalkBack and VoiceOver.
/// * [attributedDecreasedValue] for an [AttributedString] version of this
/// property.
final String? decreasedValue;
/// The [AttributedString] that [value] or [attributedValue] will become after
/// a [SemanticsAction.decrease] action has been performed on this widget.
///
/// If a [attributedDecreasedValue] is provided, [onDecrease] must also be set
/// and there must either be an ambient [Directionality] or an explicit
/// [textDirection] must be provided.
///
/// Callers must not provide both [decreasedValue] and
/// [attributedDecreasedValue], One or both must be null/// provided.
///
/// See also:
///
/// * [SemanticsConfiguration.attributedDecreasedValue] for a description of
/// how this is exposed in TalkBack and VoiceOver.
/// * [decreasedValue] for a plain string version of this property.
final AttributedString? attributedDecreasedValue;
/// Provides a brief textual description of the result of an action performed
/// on the widget.
///
/// If a hint is provided, there must either be an ambient [Directionality]
/// or an explicit [textDirection] should be provided.
///
/// Callers must not provide both [hint] and [attributedHint], One or both
/// must be null.
///
/// See also:
///
/// * [SemanticsConfiguration.hint] for a description of how this is exposed
/// in TalkBack and VoiceOver.
/// * [attributedHint] for an [AttributedString] version of this property.
final String? hint;
/// Provides an [AttributedString] version of brief textual description of the
/// result of an action performed on the widget.
///
/// If a [attributedHint] is provided, there must either by an ambient
/// [Directionality] or an explicit [textDirection] should be provided.
///
/// Callers must not provide both [hint] and [attributedHint], One or both
/// must be null.
///
/// See also:
///
/// * [SemanticsConfiguration.attributedHint] for a description of how this
/// is exposed in TalkBack and VoiceOver.
/// * [hint] for a plain string version of this property.
final AttributedString? attributedHint;
/// Provides a textual description of the widget's tooltip.
///
/// In Android, this property sets the `AccessibilityNodeInfo.setTooltipText`.
/// In iOS, this property is appended to the end of the
/// `UIAccessibilityElement.accessibilityLabel`.
///
/// If a [tooltip] is provided, there must either by an ambient
/// [Directionality] or an explicit [textDirection] should be provided.
final String? tooltip;
/// Provides hint values which override the default hints on supported
/// platforms.
///
/// On Android, If no hint overrides are used then default [hint] will be
/// combined with the [label]. Otherwise, the [hint] will be ignored as long
/// as there as at least one non-null hint override.
///
/// On iOS, these are always ignored and the default [hint] is used instead.
final SemanticsHintOverrides? hintOverrides;
/// The reading direction of the [label], [value], [increasedValue],
/// [decreasedValue], and [hint].
///
/// Defaults to the ambient [Directionality].
final TextDirection? textDirection;
/// Determines the position of this node among its siblings in the traversal
/// sort order.
///
/// This is used to describe the order in which the semantic node should be
/// traversed by the accessibility services on the platform (e.g. VoiceOver
/// on iOS and TalkBack on Android).
final SemanticsSortKey? sortKey;
/// A tag to be applied to the child [SemanticsNode]s of this widget.
///
/// The tag is added to all child [SemanticsNode]s that pass through the
/// [RenderObject] corresponding to this widget while looking to be attached
/// to a parent SemanticsNode.
///
/// Tags are used to communicate to a parent SemanticsNode that a child
/// SemanticsNode was passed through a particular RenderObject. The parent can
/// use this information to determine the shape of the semantics tree.
///
/// See also:
///
/// * [SemanticsConfiguration.addTagForChildren], to which the tags provided
/// here will be passed.
final SemanticsTag? tagForChildren;
/// The handler for [SemanticsAction.tap].
///
/// This is the semantic equivalent of a user briefly tapping the screen with
/// the finger without moving it. For example, a button should implement this
/// action.
///
/// VoiceOver users on iOS and TalkBack users on Android can trigger this
/// action by double-tapping the screen while an element is focused.
final VoidCallback? onTap;
/// The handler for [SemanticsAction.longPress].
///
/// This is the semantic equivalent of a user pressing and holding the screen
/// with the finger for a few seconds without moving it.
///
/// VoiceOver users on iOS and TalkBack users on Android can trigger this
/// action by double-tapping the screen without lifting the finger after the
/// second tap.
final VoidCallback? onLongPress;
/// The handler for [SemanticsAction.scrollLeft].
///
/// This is the semantic equivalent of a user moving their finger across the
/// screen from right to left. It should be recognized by controls that are
/// horizontally scrollable.
///
/// VoiceOver users on iOS can trigger this action by swiping left with three
/// fingers. TalkBack users on Android can trigger this action by swiping
/// right and then left in one motion path. On Android, [onScrollUp] and
/// [onScrollLeft] share the same gesture. Therefore, only on of them should
/// be provided.
final VoidCallback? onScrollLeft;
/// The handler for [SemanticsAction.scrollRight].
///
/// This is the semantic equivalent of a user moving their finger across the
/// screen from left to right. It should be recognized by controls that are
/// horizontally scrollable.
///
/// VoiceOver users on iOS can trigger this action by swiping right with three
/// fingers. TalkBack users on Android can trigger this action by swiping
/// left and then right in one motion path. On Android, [onScrollDown] and
/// [onScrollRight] share the same gesture. Therefore, only on of them should
/// be provided.
final VoidCallback? onScrollRight;
/// The handler for [SemanticsAction.scrollUp].
///
/// This is the semantic equivalent of a user moving their finger across the
/// screen from bottom to top. It should be recognized by controls that are
/// vertically scrollable.
///
/// VoiceOver users on iOS can trigger this action by swiping up with three
/// fingers. TalkBack users on Android can trigger this action by swiping
/// right and then left in one motion path. On Android, [onScrollUp] and
/// [onScrollLeft] share the same gesture. Therefore, only on of them should
/// be provided.
final VoidCallback? onScrollUp;
/// The handler for [SemanticsAction.scrollDown].
///
/// This is the semantic equivalent of a user moving their finger across the
/// screen from top to bottom. It should be recognized by controls that are
/// vertically scrollable.
///
/// VoiceOver users on iOS can trigger this action by swiping down with three
/// fingers. TalkBack users on Android can trigger this action by swiping
/// left and then right in one motion path. On Android, [onScrollDown] and
/// [onScrollRight] share the same gesture. Therefore, only on of them should
/// be provided.
final VoidCallback? onScrollDown;
/// The handler for [SemanticsAction.increase].
///
/// This is a request to increase the value represented by the widget. For
/// example, this action might be recognized by a slider control.
///
/// If a [value] is set, [increasedValue] must also be provided and
/// [onIncrease] must ensure that [value] will be set to [increasedValue].
///
/// VoiceOver users on iOS can trigger this action by swiping up with one
/// finger. TalkBack users on Android can trigger this action by pressing the
/// volume up button.
final VoidCallback? onIncrease;
/// The handler for [SemanticsAction.decrease].
///
/// This is a request to decrease the value represented by the widget. For
/// example, this action might be recognized by a slider control.
///
/// If a [value] is set, [decreasedValue] must also be provided and
/// [onDecrease] must ensure that [value] will be set to [decreasedValue].
///
/// VoiceOver users on iOS can trigger this action by swiping down with one
/// finger. TalkBack users on Android can trigger this action by pressing the
/// volume down button.
final VoidCallback? onDecrease;
/// The handler for [SemanticsAction.copy].
///
/// This is a request to copy the current selection to the clipboard.
///
/// TalkBack users on Android can trigger this action from the local context
/// menu of a text field, for example.
final VoidCallback? onCopy;
/// The handler for [SemanticsAction.cut].
///
/// This is a request to cut the current selection and place it in the
/// clipboard.
///
/// TalkBack users on Android can trigger this action from the local context
/// menu of a text field, for example.
final VoidCallback? onCut;
/// The handler for [SemanticsAction.paste].
///
/// This is a request to paste the current content of the clipboard.
///
/// TalkBack users on Android can trigger this action from the local context
/// menu of a text field, for example.
final VoidCallback? onPaste;
/// The handler for [SemanticsAction.moveCursorForwardByCharacter].
///
/// This handler is invoked when the user wants to move the cursor in a
/// text field forward by one character.
///
/// TalkBack users can trigger this by pressing the volume up key while the
/// input focus is in a text field.
final MoveCursorHandler? onMoveCursorForwardByCharacter;
/// The handler for [SemanticsAction.moveCursorBackwardByCharacter].
///
/// This handler is invoked when the user wants to move the cursor in a
/// text field backward by one character.
///
/// TalkBack users can trigger this by pressing the volume down key while the
/// input focus is in a text field.
final MoveCursorHandler? onMoveCursorBackwardByCharacter;
/// The handler for [SemanticsAction.moveCursorForwardByWord].
///
/// This handler is invoked when the user wants to move the cursor in a
/// text field backward by one word.
///
/// TalkBack users can trigger this by pressing the volume down key while the
/// input focus is in a text field.
final MoveCursorHandler? onMoveCursorForwardByWord;
/// The handler for [SemanticsAction.moveCursorBackwardByWord].
///
/// This handler is invoked when the user wants to move the cursor in a
/// text field backward by one word.
///
/// TalkBack users can trigger this by pressing the volume down key while the
/// input focus is in a text field.
final MoveCursorHandler? onMoveCursorBackwardByWord;
/// The handler for [SemanticsAction.setSelection].
///
/// This handler is invoked when the user either wants to change the currently
/// selected text in a text field or change the position of the cursor.
///
/// TalkBack users can trigger this handler by selecting "Move cursor to
/// beginning/end" or "Select all" from the local context menu.
final SetSelectionHandler? onSetSelection;
/// The handler for [SemanticsAction.setText].
///
/// This handler is invoked when the user wants to replace the current text in
/// the text field with a new text.
///
/// Voice access users can trigger this handler by speaking "type <text>" to
/// their Android devices.
final SetTextHandler? onSetText;
/// The handler for [SemanticsAction.didGainAccessibilityFocus].
///
/// This handler is invoked when the node annotated with this handler gains
/// the accessibility focus. The accessibility focus is the
/// green (on Android with TalkBack) or black (on iOS with VoiceOver)
/// rectangle shown on screen to indicate what element an accessibility
/// user is currently interacting with.
///
/// The accessibility focus is different from the input focus. The input focus
/// is usually held by the element that currently responds to keyboard inputs.
/// Accessibility focus and input focus can be held by two different nodes!
///
/// See also:
///
/// * [onDidLoseAccessibilityFocus], which is invoked when the accessibility
/// focus is removed from the node.
/// * [FocusNode], [FocusScope], [FocusManager], which manage the input focus.
final VoidCallback? onDidGainAccessibilityFocus;
/// The handler for [SemanticsAction.didLoseAccessibilityFocus].
///
/// This handler is invoked when the node annotated with this handler
/// loses the accessibility focus. The accessibility focus is
/// the green (on Android with TalkBack) or black (on iOS with VoiceOver)
/// rectangle shown on screen to indicate what element an accessibility
/// user is currently interacting with.
///
/// The accessibility focus is different from the input focus. The input focus
/// is usually held by the element that currently responds to keyboard inputs.
/// Accessibility focus and input focus can be held by two different nodes!
///
/// See also:
///
/// * [onDidGainAccessibilityFocus], which is invoked when the node gains
/// accessibility focus.
/// * [FocusNode], [FocusScope], [FocusManager], which manage the input focus.
final VoidCallback? onDidLoseAccessibilityFocus;
/// The handler for [SemanticsAction.dismiss].
///
/// This is a request to dismiss the currently focused node.
///
/// TalkBack users on Android can trigger this action in the local context
/// menu, and VoiceOver users on iOS can trigger this action with a standard
/// gesture or menu option.
final VoidCallback? onDismiss;
/// A map from each supported [CustomSemanticsAction] to a provided handler.
///
/// The handler associated with each custom action is called whenever a
/// semantics action of type [SemanticsAction.customAction] is received. The
/// provided argument will be an identifier used to retrieve an instance of
/// a custom action which can then retrieve the correct handler from this map.
///
/// See also:
///
/// * [CustomSemanticsAction], for an explanation of custom actions.
final Map<CustomSemanticsAction, VoidCallback>? customSemanticsActions;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('checked', checked, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('mixed', mixed, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('selected', selected, defaultValue: null));
properties.add(StringProperty('label', label, defaultValue: null));
properties.add(AttributedStringProperty('attributedLabel', attributedLabel, defaultValue: null));
properties.add(StringProperty('value', value, defaultValue: null));
properties.add(AttributedStringProperty('attributedValue', attributedValue, defaultValue: null));
properties.add(StringProperty('increasedValue', value, defaultValue: null));
properties.add(AttributedStringProperty('attributedIncreasedValue', attributedIncreasedValue, defaultValue: null));
properties.add(StringProperty('decreasedValue', value, defaultValue: null));
properties.add(AttributedStringProperty('attributedDecreasedValue', attributedDecreasedValue, defaultValue: null));
properties.add(StringProperty('hint', hint, defaultValue: null));
properties.add(AttributedStringProperty('attributedHint', attributedHint, defaultValue: null));
properties.add(StringProperty('tooltip', tooltip));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
properties.add(DiagnosticsProperty<SemanticsHintOverrides>('hintOverrides', hintOverrides, defaultValue: null));
}
@override
String toStringShort() => objectRuntimeType(this, 'SemanticsProperties'); // the hashCode isn't important since we're immutable
}
/// In tests use this function to reset the counter used to generate
/// [SemanticsNode.id].
void debugResetSemanticsIdCounter() {
SemanticsNode._lastIdentifier = 0;
}
/// A node that represents some semantic data.
///
/// The semantics tree is maintained during the semantics phase of the pipeline
/// (i.e., during [PipelineOwner.flushSemantics]), which happens after
/// compositing. The semantics tree is then uploaded into the engine for use
/// by assistive technology.
class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// Creates a semantic node.
///
/// Each semantic node has a unique identifier that is assigned when the node
/// is created.
SemanticsNode({
this.key,
VoidCallback? showOnScreen,
}) : _id = _generateNewId(),
_showOnScreen = showOnScreen;
/// Creates a semantic node to represent the root of the semantics tree.
///
/// The root node is assigned an identifier of zero.
SemanticsNode.root({
this.key,
VoidCallback? showOnScreen,
required SemanticsOwner owner,
}) : _id = 0,
_showOnScreen = showOnScreen {
attach(owner);
}
// The maximal semantic node identifier generated by the framework.
//
// The identifier range for semantic node IDs is split into 2, the least significant 16 bits are
// reserved for framework generated IDs(generated with _generateNewId), and most significant 32
// bits are reserved for engine generated IDs.
static const int _maxFrameworkAccessibilityIdentifier = (1<<16) - 1;
static int _lastIdentifier = 0;
static int _generateNewId() {
_lastIdentifier = (_lastIdentifier + 1) % _maxFrameworkAccessibilityIdentifier;
return _lastIdentifier;
}
/// Uniquely identifies this node in the list of sibling nodes.
///
/// Keys are used during the construction of the semantics tree. They are not
/// transferred to the engine.
final Key? key;
/// The unique identifier for this node.
///
/// The root node has an id of zero. Other nodes are given a unique id
/// when they are attached to a [SemanticsOwner]. If they are detached, their
/// ids are invalid and should not be used.
///
/// In rare circumstances, id may change if this node is detached and
/// re-attached to the [SemanticsOwner]. This should only happen when the
/// application has generated too many semantics nodes.
int get id => _id;
int _id;
final VoidCallback? _showOnScreen;
// GEOMETRY
/// The transform from this node's coordinate system to its parent's coordinate system.
///
/// By default, the transform is null, which represents the identity
/// transformation (i.e., that this node has the same coordinate system as its
/// parent).
Matrix4? get transform => _transform;
Matrix4? _transform;
set transform(Matrix4? value) {
if (!MatrixUtils.matrixEquals(_transform, value)) {
_transform = value == null || MatrixUtils.isIdentity(value) ? null : value;
_markDirty();
}
}
/// The bounding box for this node in its coordinate system.
Rect get rect => _rect;
Rect _rect = Rect.zero;
set rect(Rect value) {
assert(value.isFinite, '$this (with $owner) tried to set a non-finite rect.');
if (_rect != value) {
_rect = value;
_markDirty();
}
}
/// The semantic clip from an ancestor that was applied to this node.
///
/// Expressed in the coordinate system of the node. May be null if no clip has
/// been applied.
///
/// Descendant [SemanticsNode]s that are positioned outside of this rect will
/// be excluded from the semantics tree. Descendant [SemanticsNode]s that are
/// overlapping with this rect, but are outside of [parentPaintClipRect] will
/// be included in the tree, but they will be marked as hidden because they
/// are assumed to be not visible on screen.
///
/// If this rect is null, all descendant [SemanticsNode]s outside of
/// [parentPaintClipRect] will be excluded from the tree.
///
/// If this rect is non-null it has to completely enclose
/// [parentPaintClipRect]. If [parentPaintClipRect] is null this property is
/// also null.
Rect? parentSemanticsClipRect;
/// The paint clip from an ancestor that was applied to this node.
///
/// Expressed in the coordinate system of the node. May be null if no clip has
/// been applied.
///
/// Descendant [SemanticsNode]s that are positioned outside of this rect will
/// either be excluded from the semantics tree (if they have no overlap with
/// [parentSemanticsClipRect]) or they will be included and marked as hidden
/// (if they are overlapping with [parentSemanticsClipRect]).
///
/// This rect is completely enclosed by [parentSemanticsClipRect].
///
/// If this rect is null [parentSemanticsClipRect] also has to be null.
Rect? parentPaintClipRect;
/// The elevation adjustment that the parent imposes on this node.
///
/// The [elevation] property is relative to the elevation of the parent
/// [SemanticsNode]. However, as [SemanticsConfiguration]s from various
/// ascending [RenderObject]s are merged into each other to form that
/// [SemanticsNode] the parent’s elevation may change. This requires an
/// adjustment of the child’s relative elevation which is represented by this
/// value.
///
/// The value is rarely accessed directly. Instead, for most use cases the
/// [elevation] value should be used, which includes this adjustment.
///
/// See also:
///
/// * [elevation], the actual elevation of this [SemanticsNode].
double? elevationAdjustment;
/// The index of this node within the parent's list of semantic children.
///
/// This includes all semantic nodes, not just those currently in the
/// child list. For example, if a scrollable has five children but the first
/// two are not visible (and thus not included in the list of children), then
/// the index of the last node will still be 4.
int? indexInParent;
/// Whether the node is invisible.
///
/// A node whose [rect] is outside of the bounds of the screen and hence not
/// reachable for users is considered invisible if its semantic information
/// is not merged into a (partially) visible parent as indicated by
/// [isMergedIntoParent].
///
/// An invisible node can be safely dropped from the semantic tree without
/// loosing semantic information that is relevant for describing the content
/// currently shown on screen.
bool get isInvisible => !isMergedIntoParent && rect.isEmpty;
// MERGING
/// Whether this node merges its semantic information into an ancestor node.
bool get isMergedIntoParent => _isMergedIntoParent;
bool _isMergedIntoParent = false;
set isMergedIntoParent(bool value) {
if (_isMergedIntoParent == value) {
return;
}
_isMergedIntoParent = value;
_markDirty();
}
/// Whether this node is taking part in a merge of semantic information.
///
/// This returns true if the node is either merged into an ancestor node or if
/// decedent nodes are merged into this node.
///
/// See also:
///
/// * [isMergedIntoParent]
/// * [mergeAllDescendantsIntoThisNode]
bool get isPartOfNodeMerging => mergeAllDescendantsIntoThisNode || isMergedIntoParent;
/// Whether this node and all of its descendants should be treated as one logical entity.
bool get mergeAllDescendantsIntoThisNode => _mergeAllDescendantsIntoThisNode;
bool _mergeAllDescendantsIntoThisNode = _kEmptyConfig.isMergingSemanticsOfDescendants;
// CHILDREN
/// Contains the children in inverse hit test order (i.e. paint order).
List<SemanticsNode>? _children;
/// A snapshot of `newChildren` passed to [_replaceChildren] that we keep in
/// debug mode. It supports the assertion that user does not mutate the list
/// of children.
late List<SemanticsNode> _debugPreviousSnapshot;
void _replaceChildren(List<SemanticsNode> newChildren) {
assert(!newChildren.any((SemanticsNode child) => child == this));
assert(() {
if (identical(newChildren, _children)) {
final List<DiagnosticsNode> mutationErrors = <DiagnosticsNode>[];
if (newChildren.length != _debugPreviousSnapshot.length) {
mutationErrors.add(ErrorDescription(
"The list's length has changed from ${_debugPreviousSnapshot.length} "
'to ${newChildren.length}.',
));
} else {
for (int i = 0; i < newChildren.length; i++) {
if (!identical(newChildren[i], _debugPreviousSnapshot[i])) {
if (mutationErrors.isNotEmpty) {
mutationErrors.add(ErrorSpacer());
}
mutationErrors.add(ErrorDescription('Child node at position $i was replaced:'));
mutationErrors.add(newChildren[i].toDiagnosticsNode(name: 'Previous child', style: DiagnosticsTreeStyle.singleLine));
mutationErrors.add(_debugPreviousSnapshot[i].toDiagnosticsNode(name: 'New child', style: DiagnosticsTreeStyle.singleLine));
}
}
}
if (mutationErrors.isNotEmpty) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.'),
ErrorHint('Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.'),
ErrorDescription('Error details:'),
...mutationErrors,
]);
}
}
assert(!newChildren.any((SemanticsNode node) => node.isMergedIntoParent) || isPartOfNodeMerging);
_debugPreviousSnapshot = List<SemanticsNode>.of(newChildren);
SemanticsNode ancestor = this;
while (ancestor.parent is SemanticsNode) {
ancestor = ancestor.parent!;
}
assert(!newChildren.any((SemanticsNode child) => child == ancestor));
return true;
}());
assert(() {
final Set<SemanticsNode> seenChildren = <SemanticsNode>{};
for (final SemanticsNode child in newChildren) {
assert(seenChildren.add(child));
} // check for duplicate adds
return true;
}());
// The goal of this function is updating sawChange.
if (_children != null) {
for (final SemanticsNode child in _children!) {
child._dead = true;
}
}
for (final SemanticsNode child in newChildren) {
assert(!child.isInvisible, 'Child $child is invisible and should not be added as a child of $this.');
child._dead = false;
}
bool sawChange = false;
if (_children != null) {
for (final SemanticsNode child in _children!) {
if (child._dead) {
if (child.parent == this) {
// we might have already had our child stolen from us by
// another node that is deeper in the tree.
dropChild(child);
}
sawChange = true;
}
}
}
for (final SemanticsNode child in newChildren) {
if (child.parent != this) {
if (child.parent != null) {
// we're rebuilding the tree from the bottom up, so it's possible
// that our child was, in the last pass, a child of one of our
// ancestors. In that case, we drop the child eagerly here.
// TODO(ianh): Find a way to assert that the same node didn't
// actually appear in the tree in two places.
child.parent?.dropChild(child);
}
assert(!child.attached);
adoptChild(child);
sawChange = true;
}
}
if (!sawChange && _children != null) {
assert(newChildren.length == _children!.length);
// Did the order change?
for (int i = 0; i < _children!.length; i++) {
if (_children![i].id != newChildren[i].id) {
sawChange = true;
break;
}
}
}
_children = newChildren;
if (sawChange) {
_markDirty();
}
}
/// Whether this node has a non-zero number of children.
bool get hasChildren => _children?.isNotEmpty ?? false;
bool _dead = false;
/// The number of children this node has.
int get childrenCount => hasChildren ? _children!.length : 0;
/// Visits the immediate children of this node.
///
/// This function calls visitor for each immediate child until visitor returns
/// false. Returns true if all the visitor calls returned true, otherwise
/// returns false.
void visitChildren(SemanticsNodeVisitor visitor) {
if (_children != null) {
for (final SemanticsNode child in _children!) {
if (!visitor(child)) {
return;
}
}
}
}
/// Visit all the descendants of this node.
///
/// This function calls visitor for each descendant in a pre-order traversal
/// until visitor returns false. Returns true if all the visitor calls
/// returned true, otherwise returns false.
bool _visitDescendants(SemanticsNodeVisitor visitor) {
if (_children != null) {
for (final SemanticsNode child in _children!) {
if (!visitor(child) || !child._visitDescendants(visitor)) {
return false;
}
}
}
return true;
}
// AbstractNode OVERRIDES
@override
SemanticsOwner? get owner => super.owner as SemanticsOwner?;
@override
SemanticsNode? get parent => super.parent as SemanticsNode?;
@override
void redepthChildren() {
_children?.forEach(redepthChild);
}
@override
void attach(SemanticsOwner owner) {
super.attach(owner);
while (owner._nodes.containsKey(id)) {
// Ids may repeat if the Flutter has generated > 2^16 ids. We need to keep
// regenerating the id until we found an id that is not used.
_id = _generateNewId();
}
owner._nodes[id] = this;
owner._detachedNodes.remove(this);
if (_dirty) {
_dirty = false;
_markDirty();
}
if (_children != null) {
for (final SemanticsNode child in _children!) {
child.attach(owner);
}
}
}
@override
void detach() {
assert(owner!._nodes.containsKey(id));
assert(!owner!._detachedNodes.contains(this));
owner!._nodes.remove(id);
owner!._detachedNodes.add(this);
super.detach();
assert(owner == null);
if (_children != null) {
for (final SemanticsNode child in _children!) {
// The list of children may be stale and may contain nodes that have
// been assigned to a different parent.
if (child.parent == this) {
child.detach();
}
}
}
// The other side will have forgotten this node if we ever send
// it again, so make sure to mark it dirty so that it'll get
// sent if it is resurrected.
_markDirty();
}
// DIRTY MANAGEMENT
bool _dirty = false;
void _markDirty() {
if (_dirty) {
return;
}
_dirty = true;
if (attached) {
assert(!owner!._detachedNodes.contains(this));
owner!._dirtyNodes.add(this);
}
}
bool _isDifferentFromCurrentSemanticAnnotation(SemanticsConfiguration config) {
return _attributedLabel != config.attributedLabel
|| _attributedHint != config.attributedHint
|| _elevation != config.elevation
|| _thickness != config.thickness
|| _attributedValue != config.attributedValue
|| _attributedIncreasedValue != config.attributedIncreasedValue
|| _attributedDecreasedValue != config.attributedDecreasedValue
|| _tooltip != config.tooltip
|| _flags != config._flags
|| _textDirection != config.textDirection
|| _sortKey != config._sortKey
|| _textSelection != config._textSelection
|| _scrollPosition != config._scrollPosition
|| _scrollExtentMax != config._scrollExtentMax
|| _scrollExtentMin != config._scrollExtentMin
|| _actionsAsBits != config._actionsAsBits
|| indexInParent != config.indexInParent
|| platformViewId != config.platformViewId
|| _maxValueLength != config._maxValueLength
|| _currentValueLength != config._currentValueLength
|| _mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
}
// TAGS, LABELS, ACTIONS
Map<SemanticsAction, SemanticsActionHandler> _actions = _kEmptyConfig._actions;
Map<CustomSemanticsAction, VoidCallback> _customSemanticsActions = _kEmptyConfig._customSemanticsActions;
int _actionsAsBits = _kEmptyConfig._actionsAsBits;
/// The [SemanticsTag]s this node is tagged with.
///
/// Tags are used during the construction of the semantics tree. They are not
/// transferred to the engine.
Set<SemanticsTag>? tags;
/// Whether this node is tagged with `tag`.
bool isTagged(SemanticsTag tag) => tags != null && tags!.contains(tag);
int _flags = _kEmptyConfig._flags;
/// Whether this node currently has a given [SemanticsFlag].
bool hasFlag(SemanticsFlag flag) => _flags & flag.index != 0;
/// A textual description of this node.
///
/// The reading direction is given by [textDirection].
///
/// This exposes the raw text of the [attributedLabel].
String get label => _attributedLabel.string;
/// A textual description of this node in [AttributedString] format.
///
/// The reading direction is given by [textDirection].
///
/// See also [label], which exposes just the raw text.
AttributedString get attributedLabel => _attributedLabel;
AttributedString _attributedLabel = _kEmptyConfig.attributedLabel;
/// A textual description for the current value of the node.
///
/// The reading direction is given by [textDirection].
///
/// This exposes the raw text of the [attributedValue].
String get value => _attributedValue.string;
/// A textual description for the current value of the node in
/// [AttributedString] format.
///
/// The reading direction is given by [textDirection].
///
/// See also [value], which exposes just the raw text.
AttributedString get attributedValue => _attributedValue;
AttributedString _attributedValue = _kEmptyConfig.attributedValue;
/// The value that [value] will have after a [SemanticsAction.increase] action
/// has been performed.
///
/// This property is only valid if the [SemanticsAction.increase] action is
/// available on this node.
///
/// The reading direction is given by [textDirection].
///
/// This exposes the raw text of the [attributedIncreasedValue].
String get increasedValue => _attributedIncreasedValue.string;
/// The value in [AttributedString] format that [value] or [attributedValue]
/// will have after a [SemanticsAction.increase] action has been performed.
///
/// This property is only valid if the [SemanticsAction.increase] action is
/// available on this node.
///
/// The reading direction is given by [textDirection].
///
/// See also [increasedValue], which exposes just the raw text.
AttributedString get attributedIncreasedValue => _attributedIncreasedValue;
AttributedString _attributedIncreasedValue = _kEmptyConfig.attributedIncreasedValue;
/// The value that [value] will have after a [SemanticsAction.decrease] action
/// has been performed.
///
/// This property is only valid if the [SemanticsAction.decrease] action is
/// available on this node.
///
/// The reading direction is given by [textDirection].
///
/// This exposes the raw text of the [attributedDecreasedValue].
String get decreasedValue => _attributedDecreasedValue.string;
/// The value in [AttributedString] format that [value] or [attributedValue]
/// will have after a [SemanticsAction.decrease] action has been performed.
///
/// This property is only valid if the [SemanticsAction.decrease] action is
/// available on this node.
///
/// The reading direction is given by [textDirection].
///
/// See also [decreasedValue], which exposes just the raw text.
AttributedString get attributedDecreasedValue => _attributedDecreasedValue;
AttributedString _attributedDecreasedValue = _kEmptyConfig.attributedDecreasedValue;
/// A brief description of the result of performing an action on this node.
///
/// The reading direction is given by [textDirection].
///
/// This exposes the raw text of the [attributedHint].
String get hint => _attributedHint.string;
/// A brief description of the result of performing an action on this node
/// in [AttributedString] format.
///
/// The reading direction is given by [textDirection].
///
/// See also [hint], which exposes just the raw text.
AttributedString get attributedHint => _attributedHint;
AttributedString _attributedHint = _kEmptyConfig.attributedHint;
/// A textual description of the widget's tooltip.
///
/// The reading direction is given by [textDirection].
String get tooltip => _tooltip;
String _tooltip = _kEmptyConfig.tooltip;
/// The elevation along the z-axis at which the [rect] of this [SemanticsNode]
/// is located above its parent.
///
/// The value is relative to the parent's [elevation]. The sum of the
/// [elevation]s of all ancestor node plus this value determines the absolute
/// elevation of this [SemanticsNode].
///
/// See also:
///
/// * [thickness], which describes how much space in z-direction this
/// [SemanticsNode] occupies starting at this [elevation].
/// * [elevationAdjustment], which has been used to calculate this value.
double get elevation => _elevation;
double _elevation = _kEmptyConfig.elevation;
/// Describes how much space the [SemanticsNode] takes up along the z-axis.
///
/// A [SemanticsNode] represents multiple [RenderObject]s, which can be
/// located at various elevations in 3D. The [thickness] is the difference
/// between the absolute elevations of the lowest and highest [RenderObject]
/// represented by this [SemanticsNode]. In other words, the thickness
/// describes how high the box is that this [SemanticsNode] occupies in three
/// dimensional space. The two other dimensions are defined by [rect].
///
/// {@tool snippet}
/// The following code stacks three [PhysicalModel]s on top of each other
/// separated by non-zero elevations.
///
/// [PhysicalModel] C is elevated 10.0 above [PhysicalModel] B, which in turn
/// is elevated 5.0 above [PhysicalModel] A. The side view of this
/// constellation looks as follows:
///
/// ![A diagram illustrating the elevations of three PhysicalModels and their
/// corresponding SemanticsNodes.](https://flutter.github.io/assets-for-api-docs/assets/semantics/SemanticsNode.thickness.png)
///
/// In this example the [RenderObject]s for [PhysicalModel] C and B share one
/// [SemanticsNode] Y. Given the elevations of those [RenderObject]s, this
/// [SemanticsNode] has a [thickness] of 10.0 and an elevation of 5.0 over
/// its parent [SemanticsNode] X.
/// ```dart
/// PhysicalModel( // A
/// color: Colors.amber,
/// child: Semantics(
/// explicitChildNodes: true,
/// child: const PhysicalModel( // B
/// color: Colors.brown,
/// elevation: 5.0,
/// child: PhysicalModel( // C
/// color: Colors.cyan,
/// elevation: 10.0,
/// child: Placeholder(),
/// ),
/// ),
/// ),
/// )
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [elevation], which describes the elevation of the box defined by
/// [thickness] and [rect] relative to the parent of this [SemanticsNode].
double get thickness => _thickness;
double _thickness = _kEmptyConfig.thickness;
/// Provides hint values which override the default hints on supported
/// platforms.
SemanticsHintOverrides? get hintOverrides => _hintOverrides;
SemanticsHintOverrides? _hintOverrides;
/// The reading direction for [label], [value], [hint], [increasedValue], and
/// [decreasedValue].
TextDirection? get textDirection => _textDirection;
TextDirection? _textDirection = _kEmptyConfig.textDirection;
/// Determines the position of this node among its siblings in the traversal
/// sort order.
///
/// This is used to describe the order in which the semantic node should be
/// traversed by the accessibility services on the platform (e.g. VoiceOver
/// on iOS and TalkBack on Android).
SemanticsSortKey? get sortKey => _sortKey;
SemanticsSortKey? _sortKey;
/// The currently selected text (or the position of the cursor) within [value]
/// if this node represents a text field.
TextSelection? get textSelection => _textSelection;
TextSelection? _textSelection;
/// If this node represents a text field, this indicates whether or not it's
/// a multiline text field.
bool? get isMultiline => _isMultiline;
bool? _isMultiline;
/// The total number of scrollable children that contribute to semantics.
///
/// If the number of children are unknown or unbounded, this value will be
/// null.
int? get scrollChildCount => _scrollChildCount;
int? _scrollChildCount;
/// The index of the first visible semantic child of a scroll node.
int? get scrollIndex => _scrollIndex;
int? _scrollIndex;
/// Indicates the current scrolling position in logical pixels if the node is
/// scrollable.
///
/// The properties [scrollExtentMin] and [scrollExtentMax] indicate the valid
/// in-range values for this property. The value for [scrollPosition] may
/// (temporarily) be outside that range, e.g. during an overscroll.
///
/// See also:
///
/// * [ScrollPosition.pixels], from where this value is usually taken.
double? get scrollPosition => _scrollPosition;
double? _scrollPosition;
/// Indicates the maximum in-range value for [scrollPosition] if the node is
/// scrollable.
///
/// This value may be infinity if the scroll is unbound.
///
/// See also:
///
/// * [ScrollPosition.maxScrollExtent], from where this value is usually taken.
double? get scrollExtentMax => _scrollExtentMax;
double? _scrollExtentMax;
/// Indicates the minimum in-range value for [scrollPosition] if the node is
/// scrollable.
///
/// This value may be infinity if the scroll is unbound.
///
/// See also:
///
/// * [ScrollPosition.minScrollExtent] from where this value is usually taken.
double? get scrollExtentMin => _scrollExtentMin;
double? _scrollExtentMin;
/// The id of the platform view, whose semantics nodes will be added as
/// children to this node.
///
/// If this value is non-null, the SemanticsNode must not have any children
/// as those would be replaced by the semantics nodes of the referenced
/// platform view.
///
/// See also:
///
/// * [AndroidView], which is the platform view for Android.
/// * [UiKitView], which is the platform view for iOS.
int? get platformViewId => _platformViewId;
int? _platformViewId;
/// The maximum number of characters that can be entered into an editable
/// text field.
///
/// For the purpose of this function a character is defined as one Unicode
/// scalar value.
///
/// This should only be set when [SemanticsFlag.isTextField] is set. Defaults
/// to null, which means no limit is imposed on the text field.
int? get maxValueLength => _maxValueLength;
int? _maxValueLength;
/// The current number of characters that have been entered into an editable
/// text field.
///
/// For the purpose of this function a character is defined as one Unicode
/// scalar value.
///
/// This should only be set when [SemanticsFlag.isTextField] is set. Must be
/// set when [maxValueLength] is set.
int? get currentValueLength => _currentValueLength;
int? _currentValueLength;
bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
static final SemanticsConfiguration _kEmptyConfig = SemanticsConfiguration();
/// Reconfigures the properties of this object to describe the configuration
/// provided in the `config` argument and the children listed in the
/// `childrenInInversePaintOrder` argument.
///
/// The arguments may be null; this represents an empty configuration (all
/// values at their defaults, no children).
///
/// No reference is kept to the [SemanticsConfiguration] object, but the child
/// list is used as-is and should therefore not be changed after this call.
void updateWith({
required SemanticsConfiguration? config,
List<SemanticsNode>? childrenInInversePaintOrder,
}) {
config ??= _kEmptyConfig;
if (_isDifferentFromCurrentSemanticAnnotation(config)) {
_markDirty();
}
assert(
config.platformViewId == null || childrenInInversePaintOrder == null || childrenInInversePaintOrder.isEmpty,
'SemanticsNodes with children must not specify a platformViewId.',
);
_attributedLabel = config.attributedLabel;
_attributedValue = config.attributedValue;
_attributedIncreasedValue = config.attributedIncreasedValue;
_attributedDecreasedValue = config.attributedDecreasedValue;
_attributedHint = config.attributedHint;
_tooltip = config.tooltip;
_hintOverrides = config.hintOverrides;
_elevation = config.elevation;
_thickness = config.thickness;
_flags = config._flags;
_textDirection = config.textDirection;
_sortKey = config.sortKey;
_actions = Map<SemanticsAction, SemanticsActionHandler>.of(config._actions);
_customSemanticsActions = Map<CustomSemanticsAction, VoidCallback>.of(config._customSemanticsActions);
_actionsAsBits = config._actionsAsBits;
_textSelection = config._textSelection;
_isMultiline = config.isMultiline;
_scrollPosition = config._scrollPosition;
_scrollExtentMax = config._scrollExtentMax;
_scrollExtentMin = config._scrollExtentMin;
_mergeAllDescendantsIntoThisNode = config.isMergingSemanticsOfDescendants;
_scrollChildCount = config.scrollChildCount;
_scrollIndex = config.scrollIndex;
indexInParent = config.indexInParent;
_platformViewId = config._platformViewId;
_maxValueLength = config._maxValueLength;
_currentValueLength = config._currentValueLength;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
assert(
!_canPerformAction(SemanticsAction.increase) || (value == '') == (increasedValue == ''),
'A SemanticsNode with action "increase" needs to be annotated with either both "value" and "increasedValue" or neither',
);
assert(
!_canPerformAction(SemanticsAction.decrease) || (value == '') == (decreasedValue == ''),
'A SemanticsNode with action "decrease" needs to be annotated with either both "value" and "decreasedValue" or neither',
);
}
/// Returns a summary of the semantics for this node.
///
/// If this node has [mergeAllDescendantsIntoThisNode], then the returned data
/// includes the information from this node's descendants. Otherwise, the
/// returned data matches the data on this node.
SemanticsData getSemanticsData() {
int flags = _flags;
int actions = _actionsAsBits;
AttributedString attributedLabel = _attributedLabel;
AttributedString attributedValue = _attributedValue;
AttributedString attributedIncreasedValue = _attributedIncreasedValue;
AttributedString attributedDecreasedValue = _attributedDecreasedValue;
AttributedString attributedHint = _attributedHint;
String tooltip = _tooltip;
TextDirection? textDirection = _textDirection;
Set<SemanticsTag>? mergedTags = tags == null ? null : Set<SemanticsTag>.of(tags!);
TextSelection? textSelection = _textSelection;
int? scrollChildCount = _scrollChildCount;
int? scrollIndex = _scrollIndex;
double? scrollPosition = _scrollPosition;
double? scrollExtentMax = _scrollExtentMax;
double? scrollExtentMin = _scrollExtentMin;
int? platformViewId = _platformViewId;
int? maxValueLength = _maxValueLength;
int? currentValueLength = _currentValueLength;
final double elevation = _elevation;
double thickness = _thickness;
final Set<int> customSemanticsActionIds = <int>{};
for (final CustomSemanticsAction action in _customSemanticsActions.keys) {
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
}
if (hintOverrides != null) {
if (hintOverrides!.onTapHint != null) {
final CustomSemanticsAction action = CustomSemanticsAction.overridingAction(
hint: hintOverrides!.onTapHint!,
action: SemanticsAction.tap,
);
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
}
if (hintOverrides!.onLongPressHint != null) {
final CustomSemanticsAction action = CustomSemanticsAction.overridingAction(
hint: hintOverrides!.onLongPressHint!,
action: SemanticsAction.longPress,
);
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
}
}
if (mergeAllDescendantsIntoThisNode) {
_visitDescendants((SemanticsNode node) {
assert(node.isMergedIntoParent);
flags |= node._flags;
actions |= node._actionsAsBits;
textDirection ??= node._textDirection;
textSelection ??= node._textSelection;
scrollChildCount ??= node._scrollChildCount;
scrollIndex ??= node._scrollIndex;
scrollPosition ??= node._scrollPosition;
scrollExtentMax ??= node._scrollExtentMax;
scrollExtentMin ??= node._scrollExtentMin;
platformViewId ??= node._platformViewId;
maxValueLength ??= node._maxValueLength;
currentValueLength ??= node._currentValueLength;
if (attributedValue.string == '') {
attributedValue = node._attributedValue;
}
if (attributedIncreasedValue.string == '') {
attributedIncreasedValue = node._attributedIncreasedValue;
}
if (attributedDecreasedValue.string == '') {
attributedDecreasedValue = node._attributedDecreasedValue;
}
if (tooltip == '') {
tooltip = node._tooltip;
}
if (node.tags != null) {
mergedTags ??= <SemanticsTag>{};
mergedTags!.addAll(node.tags!);
}
for (final CustomSemanticsAction action in _customSemanticsActions.keys) {
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
}
if (node.hintOverrides != null) {
if (node.hintOverrides!.onTapHint != null) {
final CustomSemanticsAction action = CustomSemanticsAction.overridingAction(
hint: node.hintOverrides!.onTapHint!,
action: SemanticsAction.tap,
);
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
}
if (node.hintOverrides!.onLongPressHint != null) {
final CustomSemanticsAction action = CustomSemanticsAction.overridingAction(
hint: node.hintOverrides!.onLongPressHint!,
action: SemanticsAction.longPress,
);
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
}
}
attributedLabel = _concatAttributedString(
thisAttributedString: attributedLabel,
thisTextDirection: textDirection,
otherAttributedString: node._attributedLabel,
otherTextDirection: node._textDirection,
);
attributedHint = _concatAttributedString(
thisAttributedString: attributedHint,
thisTextDirection: textDirection,
otherAttributedString: node._attributedHint,
otherTextDirection: node._textDirection,
);
thickness = math.max(thickness, node._thickness + node._elevation);
return true;
});
}
return SemanticsData(
flags: flags,
actions: actions,
attributedLabel: attributedLabel,
attributedValue: attributedValue,
attributedIncreasedValue: attributedIncreasedValue,
attributedDecreasedValue: attributedDecreasedValue,
attributedHint: attributedHint,
tooltip: tooltip,
textDirection: textDirection,
rect: rect,
transform: transform,
elevation: elevation,
thickness: thickness,
tags: mergedTags,
textSelection: textSelection,
scrollChildCount: scrollChildCount,
scrollIndex: scrollIndex,
scrollPosition: scrollPosition,
scrollExtentMax: scrollExtentMax,
scrollExtentMin: scrollExtentMin,
platformViewId: platformViewId,
maxValueLength: maxValueLength,
currentValueLength: currentValueLength,
customSemanticsActionIds: customSemanticsActionIds.toList()..sort(),
);
}
static Float64List _initIdentityTransform() {
return Matrix4.identity().storage;
}
static final Int32List _kEmptyChildList = Int32List(0);
static final Int32List _kEmptyCustomSemanticsActionsList = Int32List(0);
static final Float64List _kIdentityTransform = _initIdentityTransform();
void _addToUpdate(ui.SemanticsUpdateBuilder builder, Set<int> customSemanticsActionIdsUpdate) {
assert(_dirty);
final SemanticsData data = getSemanticsData();
final Int32List childrenInTraversalOrder;
final Int32List childrenInHitTestOrder;
if (!hasChildren || mergeAllDescendantsIntoThisNode) {
childrenInTraversalOrder = _kEmptyChildList;
childrenInHitTestOrder = _kEmptyChildList;
} else {
final int childCount = _children!.length;
final List<SemanticsNode> sortedChildren = _childrenInTraversalOrder();
childrenInTraversalOrder = Int32List(childCount);
for (int i = 0; i < childCount; i += 1) {
childrenInTraversalOrder[i] = sortedChildren[i].id;
}
// _children is sorted in paint order, so we invert it to get the hit test
// order.
childrenInHitTestOrder = Int32List(childCount);
for (int i = childCount - 1; i >= 0; i -= 1) {
childrenInHitTestOrder[i] = _children![childCount - i - 1].id;
}
}
Int32List? customSemanticsActionIds;
if (data.customSemanticsActionIds?.isNotEmpty ?? false) {
customSemanticsActionIds = Int32List(data.customSemanticsActionIds!.length);
for (int i = 0; i < data.customSemanticsActionIds!.length; i++) {
customSemanticsActionIds[i] = data.customSemanticsActionIds![i];
customSemanticsActionIdsUpdate.add(data.customSemanticsActionIds![i]);
}
}
builder.updateNode(
id: id,
flags: data.flags,
actions: data.actions,
rect: data.rect,
label: data.attributedLabel.string,
labelAttributes: data.attributedLabel.attributes,
value: data.attributedValue.string,
valueAttributes: data.attributedValue.attributes,
increasedValue: data.attributedIncreasedValue.string,
increasedValueAttributes: data.attributedIncreasedValue.attributes,
decreasedValue: data.attributedDecreasedValue.string,
decreasedValueAttributes: data.attributedDecreasedValue.attributes,
hint: data.attributedHint.string,
hintAttributes: data.attributedHint.attributes,
tooltip: data.tooltip,
textDirection: data.textDirection,
textSelectionBase: data.textSelection != null ? data.textSelection!.baseOffset : -1,
textSelectionExtent: data.textSelection != null ? data.textSelection!.extentOffset : -1,
platformViewId: data.platformViewId ?? -1,
maxValueLength: data.maxValueLength ?? -1,
currentValueLength: data.currentValueLength ?? -1,
scrollChildren: data.scrollChildCount ?? 0,
scrollIndex: data.scrollIndex ?? 0 ,
scrollPosition: data.scrollPosition ?? double.nan,
scrollExtentMax: data.scrollExtentMax ?? double.nan,
scrollExtentMin: data.scrollExtentMin ?? double.nan,
transform: data.transform?.storage ?? _kIdentityTransform,
elevation: data.elevation,
thickness: data.thickness,
childrenInTraversalOrder: childrenInTraversalOrder,
childrenInHitTestOrder: childrenInHitTestOrder,
additionalActions: customSemanticsActionIds ?? _kEmptyCustomSemanticsActionsList,
);
_dirty = false;
}
/// Builds a new list made of [_children] sorted in semantic traversal order.
List<SemanticsNode> _childrenInTraversalOrder() {
TextDirection? inheritedTextDirection = textDirection;
SemanticsNode? ancestor = parent;
while (inheritedTextDirection == null && ancestor != null) {
inheritedTextDirection = ancestor.textDirection;
ancestor = ancestor.parent;
}
List<SemanticsNode>? childrenInDefaultOrder;
if (inheritedTextDirection != null) {
childrenInDefaultOrder = _childrenInDefaultOrder(_children!, inheritedTextDirection);
} else {
// In the absence of text direction default to paint order.
childrenInDefaultOrder = _children;
}
// List.sort does not guarantee stable sort order. Therefore, children are
// first partitioned into groups that have compatible sort keys, i.e. keys
// in the same group can be compared to each other. These groups stay in
// the same place. Only children within the same group are sorted.
final List<_TraversalSortNode> everythingSorted = <_TraversalSortNode>[];
final List<_TraversalSortNode> sortNodes = <_TraversalSortNode>[];
SemanticsSortKey? lastSortKey;
for (int position = 0; position < childrenInDefaultOrder!.length; position += 1) {
final SemanticsNode child = childrenInDefaultOrder[position];
final SemanticsSortKey? sortKey = child.sortKey;
lastSortKey = position > 0
? childrenInDefaultOrder[position - 1].sortKey
: null;
final bool isCompatibleWithPreviousSortKey = position == 0 ||
sortKey.runtimeType == lastSortKey.runtimeType &&
(sortKey == null || sortKey.name == lastSortKey!.name);
if (!isCompatibleWithPreviousSortKey && sortNodes.isNotEmpty) {
// Do not sort groups with null sort keys. List.sort does not guarantee
// a stable sort order.
if (lastSortKey != null) {
sortNodes.sort();
}
everythingSorted.addAll(sortNodes);
sortNodes.clear();
}
sortNodes.add(_TraversalSortNode(
node: child,
sortKey: sortKey,
position: position,
));
}
// Do not sort groups with null sort keys. List.sort does not guarantee
// a stable sort order.
if (lastSortKey != null) {
sortNodes.sort();
}
everythingSorted.addAll(sortNodes);
return everythingSorted
.map<SemanticsNode>((_TraversalSortNode sortNode) => sortNode.node)
.toList();
}
/// Sends a [SemanticsEvent] associated with this [SemanticsNode].
///
/// Semantics events should be sent to inform interested parties (like
/// the accessibility system of the operating system) about changes to the UI.
void sendEvent(SemanticsEvent event) {
if (!attached) {
return;
}
SystemChannels.accessibility.send(event.toMap(nodeId: id));
}
@override
String toStringShort() => '${objectRuntimeType(this, 'SemanticsNode')}#$id';
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
bool hideOwner = true;
if (_dirty) {
final bool inDirtyNodes = owner != null && owner!._dirtyNodes.contains(this);
properties.add(FlagProperty('inDirtyNodes', value: inDirtyNodes, ifTrue: 'dirty', ifFalse: 'STALE'));
hideOwner = inDirtyNodes;
}
properties.add(DiagnosticsProperty<SemanticsOwner>('owner', owner, level: hideOwner ? DiagnosticLevel.hidden : DiagnosticLevel.info));
properties.add(FlagProperty('isMergedIntoParent', value: isMergedIntoParent, ifTrue: 'merged up ⬆️'));
properties.add(FlagProperty('mergeAllDescendantsIntoThisNode', value: mergeAllDescendantsIntoThisNode, ifTrue: 'merge boundary ⛔️'));
final Offset? offset = transform != null ? MatrixUtils.getAsTranslation(transform!) : null;
if (offset != null) {
properties.add(DiagnosticsProperty<Rect>('rect', rect.shift(offset), showName: false));
} else {
final double? scale = transform != null ? MatrixUtils.getAsScale(transform!) : null;
String? description;
if (scale != null) {
description = '$rect scaled by ${scale.toStringAsFixed(1)}x';
} else if (transform != null && !MatrixUtils.isIdentity(transform!)) {
final String matrix = transform.toString().split('\n').take(4).map<String>((String line) => line.substring(4)).join('; ');
description = '$rect with transform [$matrix]';
}
properties.add(DiagnosticsProperty<Rect>('rect', rect, description: description, showName: false));
}
properties.add(IterableProperty<String>('tags', tags?.map((SemanticsTag tag) => tag.name), defaultValue: null));
final List<String> actions = _actions.keys.map<String>((SemanticsAction action) => describeEnum(action)).toList()..sort();
final List<String?> customSemanticsActions = _customSemanticsActions.keys
.map<String?>((CustomSemanticsAction action) => action.label)
.toList();
properties.add(IterableProperty<String>('actions', actions, ifEmpty: null));
properties.add(IterableProperty<String?>('customActions', customSemanticsActions, ifEmpty: null));
final List<String> flags = SemanticsFlag.values.values.where((SemanticsFlag flag) => hasFlag(flag)).map((SemanticsFlag flag) => flag.toString().substring('SemanticsFlag.'.length)).toList();
properties.add(IterableProperty<String>('flags', flags, ifEmpty: null));
properties.add(FlagProperty('isInvisible', value: isInvisible, ifTrue: 'invisible'));
properties.add(FlagProperty('isHidden', value: hasFlag(SemanticsFlag.isHidden), ifTrue: 'HIDDEN'));
properties.add(AttributedStringProperty('label', _attributedLabel));
properties.add(AttributedStringProperty('value', _attributedValue));
properties.add(AttributedStringProperty('increasedValue', _attributedIncreasedValue));
properties.add(AttributedStringProperty('decreasedValue', _attributedDecreasedValue));
properties.add(AttributedStringProperty('hint', _attributedHint));
properties.add(StringProperty('tooltip', _tooltip, defaultValue: ''));
properties.add(EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null));
properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
if (_textSelection?.isValid ?? false) {
properties.add(MessageProperty('text selection', '[${_textSelection!.start}, ${_textSelection!.end}]'));
}
properties.add(IntProperty('platformViewId', platformViewId, defaultValue: null));
properties.add(IntProperty('maxValueLength', maxValueLength, defaultValue: null));
properties.add(IntProperty('currentValueLength', currentValueLength, defaultValue: null));
properties.add(IntProperty('scrollChildren', scrollChildCount, defaultValue: null));
properties.add(IntProperty('scrollIndex', scrollIndex, defaultValue: null));
properties.add(DoubleProperty('scrollExtentMin', scrollExtentMin, defaultValue: null));
properties.add(DoubleProperty('scrollPosition', scrollPosition, defaultValue: null));
properties.add(DoubleProperty('scrollExtentMax', scrollExtentMax, defaultValue: null));
properties.add(DoubleProperty('elevation', elevation, defaultValue: 0.0));
properties.add(DoubleProperty('thickness', thickness, defaultValue: 0.0));
}
/// Returns a string representation of this node and its descendants.
///
/// The order in which the children of the [SemanticsNode] will be printed is
/// controlled by the [childOrder] parameter.
@override
String toStringDeep({
String prefixLineOne = '',
String? prefixOtherLines,
DiagnosticLevel minLevel = DiagnosticLevel.debug,
DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder,
}) {
return toDiagnosticsNode(childOrder: childOrder).toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines, minLevel: minLevel);
}
@override
DiagnosticsNode toDiagnosticsNode({
String? name,
DiagnosticsTreeStyle? style = DiagnosticsTreeStyle.sparse,
DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder,
}) {
return _SemanticsDiagnosticableNode(
name: name,
value: this,
style: style,
childOrder: childOrder,
);
}
@override
List<DiagnosticsNode> debugDescribeChildren({ DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.inverseHitTest }) {
return debugListChildrenInOrder(childOrder)
.map<DiagnosticsNode>((SemanticsNode node) => node.toDiagnosticsNode(childOrder: childOrder))
.toList();
}
/// Returns the list of direct children of this node in the specified order.
List<SemanticsNode> debugListChildrenInOrder(DebugSemanticsDumpOrder childOrder) {
if (_children == null) {
return const <SemanticsNode>[];
}
switch (childOrder) {
case DebugSemanticsDumpOrder.inverseHitTest:
return _children!;
case DebugSemanticsDumpOrder.traversalOrder:
return _childrenInTraversalOrder();
}
}
}
/// An edge of a box, such as top, bottom, left or right, used to compute
/// [SemanticsNode]s that overlap vertically or horizontally.
///
/// For computing horizontal overlap in an LTR setting we create two [_BoxEdge]
/// objects for each [SemanticsNode]: one representing the left edge (marked
/// with [isLeadingEdge] equal to true) and one for the right edge (with [isLeadingEdge]
/// equal to false). Similarly, for vertical overlap we also create two objects
/// for each [SemanticsNode], one for the top and one for the bottom edge.
class _BoxEdge implements Comparable<_BoxEdge> {
_BoxEdge({
required this.isLeadingEdge,
required this.offset,
required this.node,
}) : assert(offset.isFinite);
/// True if the edge comes before the seconds edge along the traversal
/// direction, and false otherwise.
///
/// This field is never null.
///
/// For example, in LTR traversal the left edge's [isLeadingEdge] is set to true,
/// the right edge's [isLeadingEdge] is set to false. When considering vertical
/// ordering of boxes, the top edge is the start edge, and the bottom edge is
/// the end edge.
final bool isLeadingEdge;
/// The offset from the start edge of the parent [SemanticsNode] in the
/// direction of the traversal.
final double offset;
/// The node whom this edge belongs.
final SemanticsNode node;
@override
int compareTo(_BoxEdge other) {
return offset.compareTo(other.offset);
}
}
/// A group of [nodes] that are disjoint vertically or horizontally from other
/// nodes that share the same [SemanticsNode] parent.
///
/// The [nodes] are sorted among each other separately from other nodes.
class _SemanticsSortGroup extends Comparable<_SemanticsSortGroup> {
_SemanticsSortGroup({
required this.startOffset,
required this.textDirection,
});
/// The offset from the start edge of the parent [SemanticsNode] in the
/// direction of the traversal.
///
/// This value is equal to the [_BoxEdge.offset] of the first node in the
/// [nodes] list being considered.
final double startOffset;
final TextDirection textDirection;
/// The nodes that are sorted among each other.
final List<SemanticsNode> nodes = <SemanticsNode>[];
@override
int compareTo(_SemanticsSortGroup other) {
return startOffset.compareTo(other.startOffset);
}
/// Sorts this group assuming that [nodes] belong to the same vertical group.
///
/// This method breaks up this group into horizontal [_SemanticsSortGroup]s
/// then sorts them using [sortedWithinKnot].
List<SemanticsNode> sortedWithinVerticalGroup() {
final List<_BoxEdge> edges = <_BoxEdge>[];
for (final SemanticsNode child in nodes) {
// Using a small delta to shrink child rects removes overlapping cases.
final Rect childRect = child.rect.deflate(0.1);
edges.add(_BoxEdge(
isLeadingEdge: true,
offset: _pointInParentCoordinates(child, childRect.topLeft).dx,
node: child,
));
edges.add(_BoxEdge(
isLeadingEdge: false,
offset: _pointInParentCoordinates(child, childRect.bottomRight).dx,
node: child,
));
}
edges.sort();
List<_SemanticsSortGroup> horizontalGroups = <_SemanticsSortGroup>[];
_SemanticsSortGroup? group;
int depth = 0;
for (final _BoxEdge edge in edges) {
if (edge.isLeadingEdge) {
depth += 1;
group ??= _SemanticsSortGroup(
startOffset: edge.offset,
textDirection: textDirection,
);
group.nodes.add(edge.node);
} else {
depth -= 1;
}
if (depth == 0) {
horizontalGroups.add(group!);
group = null;
}
}
horizontalGroups.sort();
if (textDirection == TextDirection.rtl) {
horizontalGroups = horizontalGroups.reversed.toList();
}
return horizontalGroups
.expand((_SemanticsSortGroup group) => group.sortedWithinKnot())
.toList();
}
/// Sorts [nodes] where nodes intersect both vertically and horizontally.
///
/// In the special case when [nodes] contains one or less nodes, this method
/// returns [nodes] unchanged.
///
/// This method constructs a graph, where vertices are [SemanticsNode]s and
/// edges are "traversed before" relation between pairs of nodes. The sort
/// order is the topological sorting of the graph, with the original order of
/// [nodes] used as the tie breaker.
///
/// Whether a node is traversed before another node is determined by the
/// vector that connects the two nodes' centers. If the vector "points to the
/// right or down", defined as the [Offset.direction] being between `-pi/4`
/// and `3*pi/4`), then the semantics node whose center is at the end of the
/// vector is said to be traversed after.
List<SemanticsNode> sortedWithinKnot() {
if (nodes.length <= 1) {
// Trivial knot. Nothing to do.
return nodes;
}
final Map<int, SemanticsNode> nodeMap = <int, SemanticsNode>{};
final Map<int, int> edges = <int, int>{};
for (final SemanticsNode node in nodes) {
nodeMap[node.id] = node;
final Offset center = _pointInParentCoordinates(node, node.rect.center);
for (final SemanticsNode nextNode in nodes) {
if (identical(node, nextNode) || edges[nextNode.id] == node.id) {
// Skip self or when we've already established that the next node
// points to current node.
continue;
}
final Offset nextCenter = _pointInParentCoordinates(nextNode, nextNode.rect.center);
final Offset centerDelta = nextCenter - center;
// When centers coincide, direction is 0.0.
final double direction = centerDelta.direction;
final bool isLtrAndForward = textDirection == TextDirection.ltr &&
-math.pi / 4 < direction && direction < 3 * math.pi / 4;
final bool isRtlAndForward = textDirection == TextDirection.rtl &&
(direction < -3 * math.pi / 4 || direction > 3 * math.pi / 4);
if (isLtrAndForward || isRtlAndForward) {
edges[node.id] = nextNode.id;
}
}
}
final List<int> sortedIds = <int>[];
final Set<int> visitedIds = <int>{};
final List<SemanticsNode> startNodes = nodes.toList()..sort((SemanticsNode a, SemanticsNode b) {
final Offset aTopLeft = _pointInParentCoordinates(a, a.rect.topLeft);
final Offset bTopLeft = _pointInParentCoordinates(b, b.rect.topLeft);
final int verticalDiff = aTopLeft.dy.compareTo(bTopLeft.dy);
if (verticalDiff != 0) {
return -verticalDiff;
}
return -aTopLeft.dx.compareTo(bTopLeft.dx);
});
void search(int id) {
if (visitedIds.contains(id)) {
return;
}
visitedIds.add(id);
if (edges.containsKey(id)) {
search(edges[id]!);
}
sortedIds.add(id);
}
startNodes.map<int>((SemanticsNode node) => node.id).forEach(search);
return sortedIds.map<SemanticsNode>((int id) => nodeMap[id]!).toList().reversed.toList();
}
}
/// Converts `point` to the `node`'s parent's coordinate system.
Offset _pointInParentCoordinates(SemanticsNode node, Offset point) {
if (node.transform == null) {
return point;
}
final Vector3 vector = Vector3(point.dx, point.dy, 0.0);
node.transform!.transform3(vector);
return Offset(vector.x, vector.y);
}
/// Sorts `children` using the default sorting algorithm, and returns them as a
/// new list.
///
/// The algorithm first breaks up children into groups such that no two nodes
/// from different groups overlap vertically. These groups are sorted vertically
/// according to their [_SemanticsSortGroup.startOffset].
///
/// Within each group, the nodes are sorted using
/// [_SemanticsSortGroup.sortedWithinVerticalGroup].
///
/// For an illustration of the algorithm see http://bit.ly/flutter-default-traversal.
List<SemanticsNode> _childrenInDefaultOrder(List<SemanticsNode> children, TextDirection textDirection) {
final List<_BoxEdge> edges = <_BoxEdge>[];
for (final SemanticsNode child in children) {
assert(child.rect.isFinite);
// Using a small delta to shrink child rects removes overlapping cases.
final Rect childRect = child.rect.deflate(0.1);
edges.add(_BoxEdge(
isLeadingEdge: true,
offset: _pointInParentCoordinates(child, childRect.topLeft).dy,
node: child,
));
edges.add(_BoxEdge(
isLeadingEdge: false,
offset: _pointInParentCoordinates(child, childRect.bottomRight).dy,
node: child,
));
}
edges.sort();
final List<_SemanticsSortGroup> verticalGroups = <_SemanticsSortGroup>[];
_SemanticsSortGroup? group;
int depth = 0;
for (final _BoxEdge edge in edges) {
if (edge.isLeadingEdge) {
depth += 1;
group ??= _SemanticsSortGroup(
startOffset: edge.offset,
textDirection: textDirection,
);
group.nodes.add(edge.node);
} else {
depth -= 1;
}
if (depth == 0) {
verticalGroups.add(group!);
group = null;
}
}
verticalGroups.sort();
return verticalGroups
.expand((_SemanticsSortGroup group) => group.sortedWithinVerticalGroup())
.toList();
}
/// The implementation of [Comparable] that implements the ordering of
/// [SemanticsNode]s in the accessibility traversal.
///
/// [SemanticsNode]s are sorted prior to sending them to the engine side.
///
/// This implementation considers a [node]'s [sortKey] and its position within
/// the list of its siblings. [sortKey] takes precedence over position.
class _TraversalSortNode implements Comparable<_TraversalSortNode> {
_TraversalSortNode({
required this.node,
this.sortKey,
required this.position,
});
/// The node whose position this sort node determines.
final SemanticsNode node;
/// Determines the position of this node among its siblings.
///
/// Sort keys take precedence over other attributes, such as
/// [position].
final SemanticsSortKey? sortKey;
/// Position within the list of siblings as determined by the default sort
/// order.
final int position;
@override
int compareTo(_TraversalSortNode other) {
if (sortKey == null || other.sortKey == null) {
return position - other.position;
}
return sortKey!.compareTo(other.sortKey!);
}
}
/// Owns [SemanticsNode] objects and notifies listeners of changes to the
/// render tree semantics.
///
/// To listen for semantic updates, call [SemanticsBinding.ensureSemantics] or
/// [PipelineOwner.ensureSemantics] to obtain a [SemanticsHandle]. This will
/// create a [SemanticsOwner] if necessary.
class SemanticsOwner extends ChangeNotifier {
/// Creates a [SemanticsOwner] that manages zero or more [SemanticsNode] objects.
SemanticsOwner({
required this.onSemanticsUpdate,
});