Unverified Commit 00ee2e59 authored by chunhtai's avatar chunhtai Committed by GitHub

Adds text attributes support for semantics (#79599)

* Adds label attributes

* fix test

* comments

* list equal
parent a65328b4
......@@ -3757,7 +3757,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
///
/// The [container] argument must not be null.
///
/// If the [label] is not null, the [textDirection] must also not be null.
/// If the [attributedLabel] is not null, the [textDirection] must also not be null.
RenderSemanticsAnnotations({
RenderBox? child,
bool container = false,
......@@ -3786,11 +3786,11 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool? liveRegion,
int? maxValueLength,
int? currentValueLength,
String? label,
String? value,
String? increasedValue,
String? decreasedValue,
String? hint,
AttributedString? attributedLabel,
AttributedString? attributedValue,
AttributedString? attributedIncreasedValue,
AttributedString? attributedDecreasedValue,
AttributedString? attributedHint,
SemanticsHintOverrides? hintOverrides,
TextDirection? textDirection,
SemanticsSortKey? sortKey,
......@@ -3844,11 +3844,11 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_hidden = hidden,
_image = image,
_onDismiss = onDismiss,
_label = label,
_value = value,
_increasedValue = increasedValue,
_decreasedValue = decreasedValue,
_hint = hint,
_attributedLabel = attributedLabel,
_attributedValue = attributedValue,
_attributedIncreasedValue = attributedIncreasedValue,
_attributedDecreasedValue = attributedDecreasedValue,
_attributedHint = attributedHint,
_hintOverrides = hintOverrides,
_textDirection = textDirection,
_sortKey = sortKey,
......@@ -4183,65 +4183,63 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.label] semantic to the given value.
/// If non-null, sets the [SemanticsNode.attributedLabel] semantic to the given value.
///
/// The reading direction is given by [textDirection].
String? get label => _label;
String? _label;
set label(String? value) {
if (_label == value)
AttributedString? get attributedLabel => _attributedLabel;
AttributedString? _attributedLabel;
set attributedLabel(AttributedString? value) {
if (_attributedLabel == value)
return;
_label = value;
_attributedLabel = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.value] semantic to the given value.
/// If non-null, sets the [SemanticsNode.attributedValue] semantic to the given value.
///
/// The reading direction is given by [textDirection].
String? get value => _value;
String? _value;
set value(String? value) {
if (_value == value)
AttributedString? get attributedValue => _attributedValue;
AttributedString? _attributedValue;
set attributedValue(AttributedString? value) {
if (_attributedValue == value)
return;
_value = value;
_attributedValue = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.increasedValue] semantic to the given
/// value.
/// If non-null, sets the [SemanticsNode.attributedIncreasedValue] semantic to the given value.
///
/// The reading direction is given by [textDirection].
String? get increasedValue => _increasedValue;
String? _increasedValue;
set increasedValue(String? value) {
if (_increasedValue == value)
AttributedString? get attributedIncreasedValue => _attributedIncreasedValue;
AttributedString? _attributedIncreasedValue;
set attributedIncreasedValue(AttributedString? value) {
if (_attributedIncreasedValue == value)
return;
_increasedValue = value;
_attributedIncreasedValue = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.decreasedValue] semantic to the given
/// value.
/// If non-null, sets the [SemanticsNode.attributedDecreasedValue] semantic to the given value.
///
/// The reading direction is given by [textDirection].
String? get decreasedValue => _decreasedValue;
String? _decreasedValue;
set decreasedValue(String? value) {
if (_decreasedValue == value)
AttributedString? get attributedDecreasedValue => _attributedDecreasedValue;
AttributedString? _attributedDecreasedValue;
set attributedDecreasedValue(AttributedString? value) {
if (_attributedDecreasedValue == value)
return;
_decreasedValue = value;
_attributedDecreasedValue = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.hint] semantic to the given value.
/// If non-null, sets the [SemanticsNode.attributedHint] semantic to the given value.
///
/// The reading direction is given by [textDirection].
String? get hint => _hint;
String? _hint;
set hint(String? value) {
if (_hint == value)
AttributedString? get attributedHint => _attributedHint;
AttributedString? _attributedHint;
set attributedHint(AttributedString? value) {
if (_attributedHint == value)
return;
_hint = value;
_attributedHint = value;
markNeedsSemanticsUpdate();
}
......@@ -4255,10 +4253,12 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.textDirection] semantic to the given value.
/// If non-null, sets the [SemanticsNode.textDirection] semantic to the given
/// value.
///
/// This must not be null if [label], [hint], [value], [increasedValue], or
/// [decreasedValue] are not null.
/// This must not be null if [attributedLabel], [attributedHint],
/// [attributedValue], [attributedIncreasedValue], or
/// [attributedDecreasedValue] are not null.
TextDirection? get textDirection => _textDirection;
TextDirection? _textDirection;
set textDirection(TextDirection? value) {
......@@ -4765,16 +4765,16 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.isHidden = hidden!;
if (image != null)
config.isImage = image!;
if (label != null)
config.label = label!;
if (value != null)
config.value = value!;
if (increasedValue != null)
config.increasedValue = increasedValue!;
if (decreasedValue != null)
config.decreasedValue = decreasedValue!;
if (hint != null)
config.hint = hint!;
if (attributedLabel != null)
config.attributedLabel = attributedLabel!;
if (attributedValue != null)
config.attributedValue = attributedValue!;
if (attributedIncreasedValue != null)
config.attributedIncreasedValue = attributedIncreasedValue!;
if (attributedDecreasedValue != null)
config.attributedDecreasedValue = attributedDecreasedValue!;
if (attributedHint != null)
config.attributedHint = attributedHint!;
if (hintOverrides != null && hintOverrides!.isNotEmpty)
config.hintOverrides = hintOverrides;
if (scopesRoute != null)
......
......@@ -6,7 +6,7 @@ import 'dart:math' as math;
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag,
TextDirection;
TextDirection, StringAttribute;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart' show MatrixUtils, TransformProperty;
......@@ -16,7 +16,7 @@ import 'package:vector_math/vector_math_64.dart';
import 'binding.dart' show SemanticsBinding;
import 'semantics_event.dart';
export 'dart:ui' show SemanticsAction;
export 'dart:ui' show SemanticsAction, StringAttribute, SpellOutStringAttribute, LocaleStringAttribute;
export 'semantics_event.dart';
/// Signature for a function that is called for each [SemanticsNode].
......@@ -170,6 +170,91 @@ class CustomSemanticsAction {
}
}
/// 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>.from(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 {
return ui.hashValues(
string,
attributes,
);
}
@override
String toString() {
return "${objectRuntimeType(this, 'AttributedString')}('$string', attributes: $attributes)";
}
}
/// Summary information about a [SemanticsNode] object.
///
/// A semantics node might [SemanticsNode.mergeAllDescendantsIntoThisNode],
......@@ -185,14 +270,14 @@ class SemanticsData with Diagnosticable {
/// The [flags], [actions], [label], and [Rect] arguments must not be null.
///
/// If [label] is not empty, then [textDirection] must also not be null.
const SemanticsData({
SemanticsData({
required this.flags,
required this.actions,
required this.label,
required this.increasedValue,
required this.value,
required this.decreasedValue,
required this.hint,
required this.attributedLabel,
required this.attributedValue,
required this.attributedIncreasedValue,
required this.attributedDecreasedValue,
required this.attributedHint,
required this.textDirection,
required this.rect,
required this.elevation,
......@@ -211,16 +296,16 @@ class SemanticsData with Diagnosticable {
this.customSemanticsActionIds,
}) : assert(flags != null),
assert(actions != null),
assert(label != null),
assert(value != null),
assert(decreasedValue != null),
assert(increasedValue != null),
assert(hint != null),
assert(label == '' || textDirection != null, 'A SemanticsData object with label "$label" had a null textDirection.'),
assert(value == '' || textDirection != null, 'A SemanticsData object with value "$value" had a null textDirection.'),
assert(hint == '' || textDirection != null, 'A SemanticsData object with hint "$hint" had a null textDirection.'),
assert(decreasedValue == '' || textDirection != null, 'A SemanticsData object with decreasedValue "$decreasedValue" had a null textDirection.'),
assert(increasedValue == '' || textDirection != null, 'A SemanticsData object with increasedValue "$increasedValue" had a null textDirection.'),
assert(attributedLabel != null),
assert(attributedValue != null),
assert(attributedDecreasedValue != null),
assert(attributedIncreasedValue != null),
assert(attributedHint != null),
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(attributedHint.string == '' || textDirection != null, 'A SemanticsData object with hint "${attributedHint.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(rect != null);
/// A bit field of [SemanticsFlag]s that apply to this node.
......@@ -229,32 +314,62 @@ class SemanticsData with Diagnosticable {
/// A bit field of [SemanticsAction]s that apply to this node.
final int actions;
/// A textual description of this node.
/// A textual description for the current label of the node.
///
/// The reading direction is given by [textDirection].
final String label;
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].
final AttributedString attributedLabel;
/// A textual description for the current value of the node.
///
/// The reading direction is given by [textDirection].
final String value;
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].
final AttributedString attributedValue;
/// The value that [value] will become after performing a
/// [SemanticsAction.increase] action.
///
/// The reading direction is given by [textDirection].
final String increasedValue;
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].
final AttributedString attributedIncreasedValue;
/// The value that [value] will become after performing a
/// [SemanticsAction.decrease] action.
///
/// The reading direction is given by [textDirection].
final String decreasedValue;
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].
final AttributedString attributedDecreasedValue;
/// A brief description of the result of performing an action on this node.
///
/// The reading direction is given by [textDirection].
final String hint;
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].
final AttributedString attributedHint;
/// The reading direction for the text in [label], [value], [hint],
/// [increasedValue], and [decreasedValue].
......@@ -409,11 +524,11 @@ class SemanticsData with Diagnosticable {
describeEnum(flag),
];
properties.add(IterableProperty<String>('flags', flagSummary, ifEmpty: null));
properties.add(StringProperty('label', label, defaultValue: ''));
properties.add(StringProperty('value', value, defaultValue: ''));
properties.add(StringProperty('increasedValue', increasedValue, defaultValue: ''));
properties.add(StringProperty('decreasedValue', decreasedValue, defaultValue: ''));
properties.add(StringProperty('hint', hint, defaultValue: ''));
properties.add(StringProperty('label', attributedLabel.attributes.isEmpty ? label : attributedLabel.toString(), defaultValue: ''));
properties.add(StringProperty('value', attributedValue.attributes.isEmpty? value :attributedValue.toString(), defaultValue: ''));
properties.add(StringProperty('increasedValue', attributedIncreasedValue.attributes.isEmpty? increasedValue :attributedIncreasedValue.toString(), defaultValue: ''));
properties.add(StringProperty('decreasedValue', attributedDecreasedValue.attributes.isEmpty? decreasedValue :attributedDecreasedValue.toString(), defaultValue: ''));
properties.add(StringProperty('hint', attributedHint.attributes.isEmpty? hint :attributedHint.toString(), defaultValue: ''));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
if (textSelection?.isValid == true)
properties.add(MessageProperty('textSelection', '[${textSelection!.start}, ${textSelection!.end}]'));
......@@ -432,11 +547,11 @@ class SemanticsData with Diagnosticable {
return other is SemanticsData
&& other.flags == flags
&& other.actions == actions
&& other.label == label
&& other.value == value
&& other.increasedValue == increasedValue
&& other.decreasedValue == decreasedValue
&& other.hint == hint
&& other.attributedLabel == attributedLabel
&& other.attributedValue == attributedValue
&& other.attributedIncreasedValue == attributedIncreasedValue
&& other.attributedDecreasedValue == attributedDecreasedValue
&& other.attributedHint == attributedHint
&& other.textDirection == textDirection
&& other.rect == rect
&& setEquals(other.tags, tags)
......@@ -461,11 +576,11 @@ class SemanticsData with Diagnosticable {
ui.hashValues(
flags,
actions,
label,
value,
increasedValue,
decreasedValue,
hint,
attributedLabel,
attributedValue,
attributedIncreasedValue,
attributedDecreasedValue,
attributedHint,
textDirection,
rect,
tags,
......@@ -610,10 +725,15 @@ class SemanticsProperties extends DiagnosticableTree {
this.maxValueLength,
this.currentValueLength,
this.label,
this.attributedLabel,
this.value,
this.attributedValue,
this.increasedValue,
this.attributedIncreasedValue,
this.decreasedValue,
this.attributedDecreasedValue,
this.hint,
this.attributedHint,
this.hintOverrides,
this.textDirection,
this.sortKey,
......@@ -639,7 +759,11 @@ class SemanticsProperties extends DiagnosticableTree {
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.
......@@ -846,61 +970,164 @@ class SemanticsProperties extends DiagnosticableTree {
/// 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 a [AttributedString] version of this property.
final String? label;
/// Provides a [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 a [AttributedString] version of this property.
final String? value;
/// The value that [value] will become after a [SemanticsAction.increase]
/// action has been performed on this widget.
/// Provides a [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 a [AttributedString] version of this
/// property.
final String? increasedValue;
/// The value that [value] will become after a [SemanticsAction.decrease]
/// action has been performed on this widget.
/// 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 a [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 a [AttributedString] version of this property.
final String? hint;
/// Provides a [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 hint values which override the default hints on supported
/// platforms.
///
......@@ -1183,8 +1410,11 @@ class SemanticsProperties extends DiagnosticableTree {
properties.add(DiagnosticsProperty<bool>('checked', checked, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('selected', selected, defaultValue: null));
properties.add(StringProperty('label', label, defaultValue: ''));
properties.add(StringProperty('attributedLabel', attributedLabel.toString(), defaultValue: ''));
properties.add(StringProperty('value', value));
properties.add(StringProperty('attributedValue', attributedValue.toString(), defaultValue: ''));
properties.add(StringProperty('hint', hint));
properties.add(StringProperty('attributedHint', attributedHint.toString(), defaultValue: ''));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
properties.add(DiagnosticsProperty<SemanticsHintOverrides>('hintOverrides', hintOverrides));
......@@ -1607,13 +1837,13 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
}
bool _isDifferentFromCurrentSemanticAnnotation(SemanticsConfiguration config) {
return _label != config.label ||
_hint != config.hint ||
return _attributedLabel != config.attributedLabel ||
_attributedHint != config.attributedHint ||
_elevation != config.elevation ||
_thickness != config.thickness ||
_decreasedValue != config.decreasedValue ||
_value != config.value ||
_increasedValue != config.increasedValue ||
_attributedValue != config.attributedValue ||
_attributedIncreasedValue != config.attributedIncreasedValue ||
_attributedDecreasedValue != config.attributedDecreasedValue ||
_flags != config._flags ||
_textDirection != config.textDirection ||
_sortKey != config._sortKey ||
......@@ -1653,14 +1883,25 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// A textual description of this node.
///
/// The reading direction is given by [textDirection].
String get label => _label;
String _label = _kEmptyConfig.label;
String get label => _attributedLabel.string;
/// A textual description of this node in [AttributedString] format.
///
/// The reading direction is given by [textDirection].
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].
String get value => _value;
String _value = _kEmptyConfig.value;
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].
AttributedString get attributedValue => _attributedValue;
AttributedString _attributedValue = _kEmptyConfig.attributedValue;
/// The value that [value] will have after a [SemanticsAction.decrease] action
/// has been performed.
......@@ -1669,8 +1910,17 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// available on this node.
///
/// The reading direction is given by [textDirection].
String get decreasedValue => _decreasedValue;
String _decreasedValue = _kEmptyConfig.decreasedValue;
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].
AttributedString get attributedDecreasedValue => _attributedDecreasedValue;
AttributedString _attributedDecreasedValue = _kEmptyConfig.attributedDecreasedValue;
/// The value that [value] will have after a [SemanticsAction.increase] action
/// has been performed.
......@@ -1679,14 +1929,30 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// available on this node.
///
/// The reading direction is given by [textDirection].
String get increasedValue => _increasedValue;
String _increasedValue = _kEmptyConfig.increasedValue;
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].
AttributedString get attributedIncreasedValue => _attributedIncreasedValue;
AttributedString _attributedIncreasedValue = _kEmptyConfig.attributedIncreasedValue;
/// A brief description of the result of performing an action on this node.
///
/// The reading direction is given by [textDirection].
String get hint => _hint;
String _hint = _kEmptyConfig.hint;
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].
AttributedString get attributedHint => _attributedHint;
AttributedString _attributedHint = _kEmptyConfig.attributedHint;
/// The elevation along the z-axis at which the [rect] of this [SemanticsNode]
/// is located above its parent.
......@@ -1891,11 +2157,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
'SemanticsNodes with children must not specify a platformViewId.',
);
_label = config.label;
_decreasedValue = config.decreasedValue;
_value = config.value;
_increasedValue = config.increasedValue;
_hint = config.hint;
_attributedLabel = config.attributedLabel;
_attributedValue = config.attributedValue;
_attributedIncreasedValue = config.attributedIncreasedValue;
_attributedDecreasedValue = config.attributedDecreasedValue;
_attributedHint = config.attributedHint;
_hintOverrides = config.hintOverrides;
_elevation = config.elevation;
_thickness = config.thickness;
......@@ -1920,11 +2186,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
assert(
!_canPerformAction(SemanticsAction.increase) || (_value == '') == (_increasedValue == ''),
!_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 == ''),
!_canPerformAction(SemanticsAction.decrease) || (value == '') == (decreasedValue == ''),
'A SemanticsNode with action "increase" needs to be annotated with either both "value" and "decreasedValue" or neither',
);
}
......@@ -1938,11 +2204,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
SemanticsData getSemanticsData() {
int flags = _flags;
int actions = _actionsAsBits;
String label = _label;
String hint = _hint;
String value = _value;
String increasedValue = _increasedValue;
String decreasedValue = _decreasedValue;
AttributedString attributedLabel = _attributedLabel;
AttributedString attributedValue = _attributedValue;
AttributedString attributedIncreasedValue = _attributedIncreasedValue;
AttributedString attributedDecreasedValue = _attributedDecreasedValue;
AttributedString attributedHint = _attributedHint;
TextDirection? textDirection = _textDirection;
Set<SemanticsTag>? mergedTags = tags == null ? null : Set<SemanticsTag>.from(tags!);
TextSelection? textSelection = _textSelection;
......@@ -1991,12 +2257,12 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
platformViewId ??= node._platformViewId;
maxValueLength ??= node._maxValueLength;
currentValueLength ??= node._currentValueLength;
if (value == '' || value == null)
value = node._value;
if (increasedValue == '' || increasedValue == null)
increasedValue = node._increasedValue;
if (decreasedValue == '' || decreasedValue == null)
decreasedValue = node._decreasedValue;
if (attributedValue == null || attributedValue.string == '')
attributedValue = node._attributedValue;
if (attributedIncreasedValue == null || attributedIncreasedValue.string == '')
attributedIncreasedValue = node._attributedIncreasedValue;
if (attributedDecreasedValue == null || attributedDecreasedValue.string == '')
attributedDecreasedValue = node._attributedDecreasedValue;
if (node.tags != null) {
mergedTags ??= <SemanticsTag>{};
mergedTags!.addAll(node.tags!);
......@@ -2019,16 +2285,16 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
}
}
label = _concatStrings(
thisString: label,
attributedLabel = _concatAttributedString(
thisAttributedString: attributedLabel,
thisTextDirection: textDirection,
otherString: node._label,
otherAttributedString: node._attributedLabel,
otherTextDirection: node._textDirection,
);
hint = _concatStrings(
thisString: hint,
attributedHint = _concatAttributedString(
thisAttributedString: attributedHint,
thisTextDirection: textDirection,
otherString: node._hint,
otherAttributedString: node._attributedHint,
otherTextDirection: node._textDirection,
);
......@@ -2041,11 +2307,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
return SemanticsData(
flags: flags,
actions: actions,
label: label,
value: value,
increasedValue: increasedValue,
decreasedValue: decreasedValue,
hint: hint,
attributedLabel: attributedLabel,
attributedValue: attributedValue,
attributedIncreasedValue: attributedIncreasedValue,
attributedDecreasedValue: attributedDecreasedValue,
attributedHint: attributedHint,
textDirection: textDirection,
rect: rect,
transform: transform,
......@@ -2108,11 +2374,16 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
flags: data.flags,
actions: data.actions,
rect: data.rect,
label: data.label,
value: data.value,
decreasedValue: data.decreasedValue,
increasedValue: data.increasedValue,
hint: data.hint,
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,
textDirection: data.textDirection,
textSelectionBase: data.textSelection != null ? data.textSelection!.baseOffset : -1,
textSelectionExtent: data.textSelection != null ? data.textSelection!.extentOffset : -1,
......@@ -2246,11 +2517,11 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
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(StringProperty('label', _label, defaultValue: ''));
properties.add(StringProperty('value', _value, defaultValue: ''));
properties.add(StringProperty('increasedValue', _increasedValue, defaultValue: ''));
properties.add(StringProperty('decreasedValue', _decreasedValue, defaultValue: ''));
properties.add(StringProperty('hint', _hint, defaultValue: ''));
properties.add(StringProperty('label', _attributedLabel.attributes.isEmpty ? _attributedLabel.string : _attributedLabel.toString(), defaultValue: ''));
properties.add(StringProperty('value', _attributedValue.attributes.isEmpty ? _attributedValue.string : _attributedValue.toString(), defaultValue: ''));
properties.add(StringProperty('increasedValue', _attributedIncreasedValue.attributes.isEmpty ? _attributedIncreasedValue.string : _attributedIncreasedValue.toString(), defaultValue: ''));
properties.add(StringProperty('decreasedValue', _attributedDecreasedValue.attributes.isEmpty ? _attributedDecreasedValue.string : _attributedDecreasedValue.toString(), defaultValue: ''));
properties.add(StringProperty('hint', _attributedHint.attributes.isEmpty ? _attributedHint.string : _attributedHint.toString(), defaultValue: ''));
properties.add(EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null));
properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
if (_textSelection?.isValid == true)
......@@ -3414,86 +3685,174 @@ class SemanticsConfiguration {
/// A textual description of the owning [RenderObject].
///
/// On iOS this is used for the `accessibilityLabel` property defined in the
/// `UIAccessibility` Protocol. On Android it is concatenated together with
/// [value] and [hint] in the following order: [value], [label], [hint].
/// The concatenated value is then used as the `Text` description.
/// Setting this attribute will override the [attributedLabel].
///
/// The reading direction is given by [textDirection].
String get label => _label;
String _label = '';
///
/// See also:
/// * [attributedLabel]: which is the [AttributedString] of this property.
String get label => _attributedLabel.string;
set label(String label) {
assert(label != null);
_label = label;
_attributedLabel = AttributedString(label);
_hasBeenAnnotated = true;
}
/// A textual description of the owning [RenderObject] in [AttributedString]
/// format.
///
/// On iOS this is used for the `accessibilityAttributedLabel` property
/// defined in the `UIAccessibility` Protocol. On Android it is concatenated
/// together with [attributedValue] and [attributedHint] in the following
/// order: [attributedValue], [attributedLabel], [attributedHint]. The
/// concatenated value is then used as the `Text` description.
///
/// The reading direction is given by [textDirection].
AttributedString get attributedLabel => _attributedLabel;
AttributedString _attributedLabel = AttributedString('');
set attributedLabel(AttributedString attributedLabel) {
_attributedLabel = attributedLabel;
_hasBeenAnnotated = true;
}
/// A textual description for the current value of the owning [RenderObject].
///
/// On iOS this is used for the `accessibilityValue` property defined in the
/// `UIAccessibility` Protocol. On Android it is concatenated together with
/// [label] and [hint] in the following order: [value], [label], [hint].
/// The concatenated value is then used as the `Text` description.
/// Setting this attribute will override the [attributedValue].
///
/// The reading direction is given by [textDirection].
///
/// See also:
///
/// * [decreasedValue], describes what [value] will be after performing
/// [SemanticsAction.decrease].
/// * [increasedValue], describes what [value] will be after performing
/// [SemanticsAction.increase].
String get value => _value;
String _value = '';
/// * [attributedValue], which is the [AttributedString] of this property.
/// * [decreasedValue] and [attributedDecreasedValue], describes what
/// [value] will be after performing [SemanticsAction.decrease].
/// * [increasedValue] and [attributedIncreasedValue], describes what
/// [value] will be after performing [SemanticsAction.increase].
String get value => _attributedValue.string;
set value(String value) {
assert(value != null);
_value = value;
_attributedValue = AttributedString(value);
_hasBeenAnnotated = true;
}
/// A textual description for the current value of the owning [RenderObject]
/// in [AttributedString] format.
///
/// On iOS this is used for the `accessibilityAttributedValue` property
/// defined in the `UIAccessibility` Protocol. On Android it is concatenated
/// together with [attributedLabel] and [attributedHint] in the following
/// order: [attributedValue], [attributedLabel], [attributedHint]. The
/// concatenated value is then used as the `Text` description.
///
/// The reading direction is given by [textDirection].
///
/// See also:
///
/// * [attributedDecreasedValue], describes what [value] will be after
/// performing [SemanticsAction.decrease].
/// * [attributedIncreasedValue], describes what [value] will be after
/// performing [SemanticsAction.increase].
AttributedString get attributedValue => _attributedValue;
AttributedString _attributedValue = AttributedString('');
set attributedValue(AttributedString attributedValue) {
_attributedValue = attributedValue;
_hasBeenAnnotated = true;
}
/// The value that [value] will have after performing a
/// [SemanticsAction.decrease] action.
///
/// This must be set if a handler for [SemanticsAction.decrease] is provided
/// and [value] is set.
/// Setting this attribute will override the [attributedDecreasedValue].
///
/// One of the [attributedDecreasedValue] or [decreasedValue] must be set if
/// a handler for [SemanticsAction.decrease] is provided and one of the
/// [value] or [attributedValue] is set.
///
/// The reading direction is given by [textDirection].
String get decreasedValue => _decreasedValue;
String _decreasedValue = '';
String get decreasedValue => _attributedDecreasedValue.string;
set decreasedValue(String decreasedValue) {
assert(decreasedValue != null);
_decreasedValue = decreasedValue;
_attributedDecreasedValue = AttributedString(decreasedValue);
_hasBeenAnnotated = true;
}
/// The value that [value] will have after performing a
/// [SemanticsAction.decrease] action in [AttributedString] format.
///
/// One of the [attributedDecreasedValue] or [decreasedValue] must be set if
/// a handler for [SemanticsAction.decrease] is provided and one of the
/// [value] or [attributedValue] is set.
///
/// The reading direction is given by [textDirection].
AttributedString get attributedDecreasedValue => _attributedDecreasedValue;
AttributedString _attributedDecreasedValue = AttributedString('');
set attributedDecreasedValue(AttributedString attributedDecreasedValue) {
_attributedDecreasedValue = attributedDecreasedValue;
_hasBeenAnnotated = true;
}
/// The value that [value] will have after performing a
/// [SemanticsAction.increase] action.
///
/// This must be set if a handler for [SemanticsAction.increase] is provided
/// and [value] is set.
/// Setting this attribute will override the [attributedIncreasedValue].
///
/// One of the [attributedIncreasedValue] or [increasedValue] must be set if
/// a handler for [SemanticsAction.increase] is provided and one of the
/// [value] or [attributedValue] is set.
///
/// The reading direction is given by [textDirection].
String get increasedValue => _increasedValue;
String _increasedValue = '';
String get increasedValue => _attributedIncreasedValue.string;
set increasedValue(String increasedValue) {
assert(increasedValue != null);
_increasedValue = increasedValue;
_attributedIncreasedValue = AttributedString(increasedValue);
_hasBeenAnnotated = true;
}
/// The value that [value] will have after performing a
/// [SemanticsAction.increase] action in [AttributedString] format.
///
/// One of the [attributedIncreasedValue] or [increasedValue] must be set if
/// a handler for [SemanticsAction.increase] is provided and one of the
/// [value] or [attributedValue] is set.
///
/// The reading direction is given by [textDirection].
AttributedString get attributedIncreasedValue => _attributedIncreasedValue;
AttributedString _attributedIncreasedValue = AttributedString('');
set attributedIncreasedValue(AttributedString attributedIncreasedValue) {
_attributedIncreasedValue = attributedIncreasedValue;
_hasBeenAnnotated = true;
}
/// A brief description of the result of performing an action on this node.
///
/// On iOS this is used for the `accessibilityHint` property defined in the
/// `UIAccessibility` Protocol. On Android it is concatenated together with
/// [label] and [value] in the following order: [value], [label], [hint].
/// The concatenated value is then used as the `Text` description.
/// Setting this attribute will override the [attributedHint].
///
/// The reading direction is given by [textDirection].
String get hint => _hint;
String _hint = '';
///
/// See also:
/// * [attributedHint]: which is the [AttributedString] of this property.
String get hint => _attributedHint.string;
set hint(String hint) {
assert(hint != null);
_hint = hint;
_attributedHint = AttributedString(hint);
_hasBeenAnnotated = true;
}
/// A brief description of the result of performing an action on this node in
/// [AttributedString] format.
///
/// On iOS this is used for the `accessibilityAttributedHint` property
/// defined in the `UIAccessibility` Protocol. On Android it is concatenated
/// together with [attributedLabel] and [attributedValue] in the following
/// order: [attributedValue], [attributedLabel], [attributedHint]. The
/// concatenated value is then used as the `Text` description.
///
/// The reading direction is given by [textDirection].
AttributedString get attributedHint => _attributedHint;
AttributedString _attributedHint = AttributedString('');
set attributedHint(AttributedString attributedHint) {
_attributedHint = attributedHint;
_hasBeenAnnotated = true;
}
......@@ -3903,7 +4262,7 @@ class SemanticsConfiguration {
if (_currentValueLength != null && other._currentValueLength != null) {
return false;
}
if (_value != null && _value.isNotEmpty && other._value != null && other._value.isNotEmpty)
if (_attributedValue != null && _attributedValue.string.isNotEmpty && other._attributedValue != null && other._attributedValue.string.isNotEmpty)
return false;
return true;
}
......@@ -3943,22 +4302,22 @@ class SemanticsConfiguration {
textDirection ??= child.textDirection;
_sortKey ??= child._sortKey;
_label = _concatStrings(
thisString: _label,
_attributedLabel = _concatAttributedString(
thisAttributedString: _attributedLabel,
thisTextDirection: textDirection,
otherString: child._label,
otherAttributedString: child._attributedLabel,
otherTextDirection: child.textDirection,
);
if (_decreasedValue == '' || _decreasedValue == null)
_decreasedValue = child._decreasedValue;
if (_value == '' || _value == null)
_value = child._value;
if (_increasedValue == '' || _increasedValue == null)
_increasedValue = child._increasedValue;
_hint = _concatStrings(
thisString: _hint,
if (_attributedValue == null || _attributedValue.string == '')
_attributedValue = child._attributedValue;
if (_attributedIncreasedValue == null || _attributedIncreasedValue.string == '')
_attributedIncreasedValue = child._attributedIncreasedValue;
if (_attributedDecreasedValue == null || _attributedDecreasedValue.string == '')
_attributedDecreasedValue = child._attributedDecreasedValue;
_attributedHint = _concatAttributedString(
thisAttributedString: _attributedHint,
thisTextDirection: textDirection,
otherString: child._hint,
otherAttributedString: child._attributedHint,
otherTextDirection: child.textDirection,
);
......@@ -3977,11 +4336,11 @@ class SemanticsConfiguration {
.._isMergingSemanticsOfDescendants = _isMergingSemanticsOfDescendants
.._textDirection = _textDirection
.._sortKey = _sortKey
.._label = _label
.._increasedValue = _increasedValue
.._value = _value
.._decreasedValue = _decreasedValue
.._hint = _hint
.._attributedLabel = _attributedLabel
.._attributedIncreasedValue = _attributedIncreasedValue
.._attributedValue = _attributedValue
.._attributedDecreasedValue = _attributedDecreasedValue
.._attributedHint = _attributedHint
.._hintOverrides = _hintOverrides
.._elevation = _elevation
.._thickness = _thickness
......@@ -4020,28 +4379,28 @@ enum DebugSemanticsDumpOrder {
traversalOrder,
}
String _concatStrings({
required String thisString,
required String otherString,
AttributedString _concatAttributedString({
required AttributedString thisAttributedString,
required AttributedString otherAttributedString,
required TextDirection? thisTextDirection,
required TextDirection? otherTextDirection,
}) {
if (otherString.isEmpty)
return thisString;
String nestedLabel = otherString;
if (otherAttributedString.string.isEmpty)
return thisAttributedString;
if (thisTextDirection != otherTextDirection && otherTextDirection != null) {
switch (otherTextDirection) {
case TextDirection.rtl:
nestedLabel = '${Unicode.RLE}$nestedLabel${Unicode.PDF}';
otherAttributedString = AttributedString(Unicode.RLE) + otherAttributedString + AttributedString(Unicode.PDF);
break;
case TextDirection.ltr:
nestedLabel = '${Unicode.LRE}$nestedLabel${Unicode.PDF}';
otherAttributedString = AttributedString(Unicode.LRE) + otherAttributedString + AttributedString(Unicode.PDF);
break;
}
}
if (thisString.isEmpty)
return nestedLabel;
return '$thisString\n$nestedLabel';
if (thisAttributedString.string.isEmpty)
return otherAttributedString;
return thisAttributedString + AttributedString('\n') + otherAttributedString;
}
/// Base class for all sort keys for [SemanticsProperties.sortKey] accessibility
......
......@@ -7305,10 +7305,15 @@ class Semantics extends SingleChildRenderObjectWidget {
int? maxValueLength,
int? currentValueLength,
String? label,
AttributedString? attributedLabel,
String? value,
AttributedString? attributedValue,
String? increasedValue,
AttributedString? attributedIncreasedValue,
String? decreasedValue,
AttributedString? attributedDecreasedValue,
String? hint,
AttributedString? attributedHint,
String? onTapHint,
String? onLongPressHint,
TextDirection? textDirection,
......@@ -7364,10 +7369,15 @@ class Semantics extends SingleChildRenderObjectWidget {
maxValueLength: maxValueLength,
currentValueLength: currentValueLength,
label: label,
attributedLabel: attributedLabel,
value: value,
attributedValue: attributedValue,
increasedValue: increasedValue,
attributedIncreasedValue: attributedIncreasedValue,
decreasedValue: decreasedValue,
attributedDecreasedValue: attributedDecreasedValue,
hint: hint,
attributedHint: attributedHint,
textDirection: textDirection,
sortKey: sortKey,
tagForChildren: tagForChildren,
......@@ -7452,6 +7462,31 @@ class Semantics extends SingleChildRenderObjectWidget {
/// an [ExcludeSemantics] widget and then another [Semantics] widget.
final bool excludeSemantics;
AttributedString? get _effectiveAttributedLabel {
return properties.attributedLabel ??
(properties.label == null ? null : AttributedString(properties.label!));
}
AttributedString? get _effectiveAttributedValue {
return properties.attributedValue ??
(properties.value == null ? null : AttributedString(properties.value!));
}
AttributedString? get _effectiveAttributedIncreasedValue {
return properties.attributedIncreasedValue ??
(properties.increasedValue == null ? null : AttributedString(properties.increasedValue!));
}
AttributedString? get _effectiveAttributedDecreasedValue {
return properties.attributedDecreasedValue ??
(properties.decreasedValue == null ? null : AttributedString(properties.decreasedValue!));
}
AttributedString? get _effectiveAttributedHint {
return properties.attributedHint ??
(properties.hint == null ? null : AttributedString(properties.hint!));
}
@override
RenderSemanticsAnnotations createRenderObject(BuildContext context) {
return RenderSemanticsAnnotations(
......@@ -7481,11 +7516,11 @@ class Semantics extends SingleChildRenderObjectWidget {
namesRoute: properties.namesRoute,
hidden: properties.hidden,
image: properties.image,
label: properties.label,
value: properties.value,
increasedValue: properties.increasedValue,
decreasedValue: properties.decreasedValue,
hint: properties.hint,
attributedLabel: _effectiveAttributedLabel,
attributedValue: _effectiveAttributedValue,
attributedIncreasedValue: _effectiveAttributedIncreasedValue,
attributedDecreasedValue: _effectiveAttributedDecreasedValue,
attributedHint: _effectiveAttributedHint,
hintOverrides: properties.hintOverrides,
textDirection: _getTextDirection(context),
sortKey: properties.sortKey,
......@@ -7518,7 +7553,10 @@ class Semantics extends SingleChildRenderObjectWidget {
if (properties.textDirection != null)
return properties.textDirection;
final bool containsText = properties.label != null || properties.value != null || properties.hint != null;
final bool containsText = properties.attributedLabel != null ||
properties.label != null ||
properties.value != null ||
properties.hint != null;
if (!containsText)
return null;
......@@ -7554,11 +7592,11 @@ class Semantics extends SingleChildRenderObjectWidget {
..liveRegion = properties.liveRegion
..maxValueLength = properties.maxValueLength
..currentValueLength = properties.currentValueLength
..label = properties.label
..value = properties.value
..increasedValue = properties.increasedValue
..decreasedValue = properties.decreasedValue
..hint = properties.hint
..attributedLabel = _effectiveAttributedLabel
..attributedValue = _effectiveAttributedValue
..attributedIncreasedValue = _effectiveAttributedIncreasedValue
..attributedDecreasedValue = _effectiveAttributedDecreasedValue
..attributedHint = _effectiveAttributedHint
..hintOverrides = properties.hintOverrides
..namesRoute = properties.namesRoute
..textDirection = _getTextDirection(context)
......
......@@ -280,22 +280,22 @@ class _SemanticsDebuggerPainter extends CustomPainter {
if (isAdjustable)
annotations.add('adjustable');
assert(data.label != null);
assert(data.attributedLabel != null);
final String message;
if (data.label.isEmpty) {
if (data.attributedLabel.string.isEmpty) {
message = annotations.join('; ');
} else {
final String label;
if (data.textDirection == null) {
label = '${Unicode.FSI}${data.label}${Unicode.PDI}';
label = '${Unicode.FSI}${data.attributedLabel.string}${Unicode.PDI}';
annotations.insert(0, 'MISSING TEXT DIRECTION');
} else {
switch (data.textDirection!) {
case TextDirection.rtl:
label = '${Unicode.RLI}${data.label}${Unicode.PDF}';
label = '${Unicode.RLI}${data.attributedLabel.string}${Unicode.PDF}';
break;
case TextDirection.ltr:
label = data.label;
label = data.attributedLabel.string;
break;
}
}
......
......@@ -28,7 +28,7 @@ class TestTree {
child: RenderPositionedBox(
child: child = RenderConstrainedBox(
additionalConstraints: const BoxConstraints.tightFor(height: 20.0, width: 20.0),
child: RenderSemanticsAnnotations(label: 'Hello there foo', textDirection: TextDirection.ltr),
child: RenderSemanticsAnnotations(attributedLabel: AttributedString('Hello there foo'), textDirection: TextDirection.ltr),
),
),
),
......
......@@ -11,7 +11,7 @@ import 'rendering_tester.dart';
void main() {
test('only send semantics update if semantics have changed', () {
final TestRender testRender = TestRender()
..label = 'hello'
..attributedLabel = AttributedString('hello')
..textDirection = TextDirection.ltr;
final RenderConstrainedBox tree = RenderConstrainedBox(
......@@ -46,7 +46,7 @@ void main() {
semanticsUpdateCount = 0;
// Change semantics and request update.
testRender.label = 'bye';
testRender.attributedLabel = AttributedString('bye');
testRender.markNeedsSemanticsUpdate();
pumpFrame(phase: EnginePhase.flushSemantics);
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart';
......@@ -63,6 +64,75 @@ void main() {
expect(node.getSemanticsData().tags, tags);
});
test('SemanticsConfiguration can set both string label/value/hint and attributed version', () {
final SemanticsConfiguration config = SemanticsConfiguration();
config.label = 'label1';
expect(config.label, 'label1');
expect(config.attributedLabel.string, 'label1');
expect(config.attributedLabel.attributes.isEmpty, isTrue);
config.attributedLabel = AttributedString(
'label2',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end:1)),
]
);
expect(config.label, 'label2');
expect(config.attributedLabel.string, 'label2');
expect(config.attributedLabel.attributes.length, 1);
expect(config.attributedLabel.attributes[0] is SpellOutStringAttribute, isTrue);
expect(config.attributedLabel.attributes[0].range, const TextRange(start: 0, end: 1));
config.label = 'label3';
expect(config.label, 'label3');
expect(config.attributedLabel.string, 'label3');
expect(config.attributedLabel.attributes.isEmpty, isTrue);
config.value = 'value1';
expect(config.value, 'value1');
expect(config.attributedValue.string, 'value1');
expect(config.attributedValue.attributes.isEmpty, isTrue);
config.attributedValue = AttributedString(
'value2',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end:1)),
]
);
expect(config.value, 'value2');
expect(config.attributedValue.string, 'value2');
expect(config.attributedValue.attributes.length, 1);
expect(config.attributedValue.attributes[0] is SpellOutStringAttribute, isTrue);
expect(config.attributedValue.attributes[0].range, const TextRange(start: 0, end: 1));
config.value = 'value3';
expect(config.value, 'value3');
expect(config.attributedValue.string, 'value3');
expect(config.attributedValue.attributes.isEmpty, isTrue);
config.hint = 'hint1';
expect(config.hint, 'hint1');
expect(config.attributedHint.string, 'hint1');
expect(config.attributedHint.attributes.isEmpty, isTrue);
config.attributedHint = AttributedString(
'hint2',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end:1)),
]
);
expect(config.hint, 'hint2');
expect(config.attributedHint.string, 'hint2');
expect(config.attributedHint.attributes.length, 1);
expect(config.attributedHint.attributes[0] is SpellOutStringAttribute, isTrue);
expect(config.attributedHint.attributes[0].range, const TextRange(start: 0, end: 1));
config.hint = 'hint3';
expect(config.hint, 'hint3');
expect(config.attributedHint.string, 'hint3');
expect(config.attributedHint.attributes.isEmpty, isTrue);
});
test('mutate existing semantic node list errors', () {
final SemanticsNode node = SemanticsNode()
..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
......@@ -570,6 +640,26 @@ void main() {
);
});
test('Attributed String can concate', () {
final AttributedString string1 = AttributedString(
'string1',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start:0, end:4)),
]
);
final AttributedString string2 = AttributedString(
'string2',
attributes: <StringAttribute>[
LocaleStringAttribute(locale: const Locale('es', 'MX'), range: const TextRange(start:0, end:4)),
]
);
final AttributedString result = string1 + string2;
expect(result.string, 'string1string2');
expect(result.attributes.length, 2);
expect(result.attributes[0].range, const TextRange(start:0, end:4));
expect(result.attributes[0] is SpellOutStringAttribute, isTrue);
});
test('Semantics id does not repeat', () {
final SemanticsOwner owner = SemanticsOwner();
const int expectId = 1400;
......
......@@ -6,6 +6,7 @@ import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
......@@ -83,6 +84,76 @@ void main() {
SemanticsUpdateBuilderSpy.observations.clear();
handle.dispose();
});
testWidgets('Semantics update receives attributed text', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
// Pumps a placeholder to trigger the warm up frame.
await tester.pumpWidget(
const Placeholder(),
// Stops right after the warm up frame.
null,
EnginePhase.build,
);
// The warm up frame will send update for an empty semantics tree. We
// ignore this one time update.
SemanticsUpdateBuilderSpy.observations.clear();
// Builds the real widget tree.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
attributedLabel: AttributedString(
'label',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
],
),
attributedValue: AttributedString(
'value',
attributes: <StringAttribute>[
LocaleStringAttribute(range: const TextRange(start: 0, end: 5), locale: const Locale('en', 'MX')),
],
),
attributedHint: AttributedString(
'hint',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)),
],
),
child: const Placeholder(),
),
),
);
expect(SemanticsUpdateBuilderSpy.observations.length, 2);
expect(SemanticsUpdateBuilderSpy.observations.containsKey(0), isTrue);
expect(SemanticsUpdateBuilderSpy.observations[0]!.childrenInTraversalOrder.length, 1);
expect(SemanticsUpdateBuilderSpy.observations[0]!.childrenInTraversalOrder[0], 1);
expect(SemanticsUpdateBuilderSpy.observations.containsKey(1), isTrue);
expect(SemanticsUpdateBuilderSpy.observations[1]!.childrenInTraversalOrder.length, 0);
expect(SemanticsUpdateBuilderSpy.observations[1]!.label, 'label');
expect(SemanticsUpdateBuilderSpy.observations[1]!.labelAttributes!.length, 1);
expect(SemanticsUpdateBuilderSpy.observations[1]!.labelAttributes![0] is SpellOutStringAttribute, isTrue);
expect(SemanticsUpdateBuilderSpy.observations[1]!.labelAttributes![0].range, const TextRange(start: 0, end: 5));
expect(SemanticsUpdateBuilderSpy.observations[1]!.value, 'value');
expect(SemanticsUpdateBuilderSpy.observations[1]!.valueAttributes!.length, 1);
expect(SemanticsUpdateBuilderSpy.observations[1]!.valueAttributes![0] is LocaleStringAttribute, isTrue);
final LocaleStringAttribute localeAttribute = SemanticsUpdateBuilderSpy.observations[1]!.valueAttributes![0] as LocaleStringAttribute;
expect(localeAttribute.range, const TextRange(start: 0, end: 5));
expect(localeAttribute.locale, const Locale('en', 'MX'));
expect(SemanticsUpdateBuilderSpy.observations[1]!.hint, 'hint');
expect(SemanticsUpdateBuilderSpy.observations[1]!.hintAttributes!.length, 1);
expect(SemanticsUpdateBuilderSpy.observations[1]!.hintAttributes![0] is SpellOutStringAttribute, isTrue);
expect(SemanticsUpdateBuilderSpy.observations[1]!.hintAttributes![0].range, const TextRange(start: 1, end: 2));
SemanticsUpdateBuilderSpy.observations.clear();
handle.dispose();
});
}
class SemanticsUpdateTestBinding extends AutomatedTestWidgetsFlutterBinding {
......@@ -114,18 +185,15 @@ class SemanticsUpdateBuilderSpy extends ui.SemanticsUpdateBuilder {
required double thickness,
required Rect rect,
required String label,
// TODO(chunhtai): change the Object? to List<StringAttribute> when engine
// pr lands: https://github.com/flutter/engine/pull/25373.
// https://github.com/flutter/flutter/issues/79318.
Object? labelAttributes,
List<ui.StringAttribute>? labelAttributes,
required String value,
Object? valueAttributes,
List<ui.StringAttribute>? valueAttributes,
required String increasedValue,
Object? increasedValueAttributes,
List<ui.StringAttribute>? increasedValueAttributes,
required String decreasedValue,
Object? decreasedValueAttributes,
List<ui.StringAttribute>? decreasedValueAttributes,
required String hint,
Object? hintAttributes,
List<ui.StringAttribute>? hintAttributes,
TextDirection? textDirection,
required Float64List transform,
required Int32List childrenInTraversalOrder,
......@@ -152,10 +220,15 @@ class SemanticsUpdateBuilderSpy extends ui.SemanticsUpdateBuilder {
thickness: thickness,
rect: rect,
label: label,
labelAttributes: labelAttributes,
hint: hint,
hintAttributes: hintAttributes,
value: value,
valueAttributes: valueAttributes,
increasedValue: increasedValue,
increasedValueAttributes: increasedValueAttributes,
decreasedValue: decreasedValue,
decreasedValueAttributes: decreasedValueAttributes,
textDirection: textDirection,
transform: transform,
childrenInTraversalOrder: childrenInTraversalOrder,
......@@ -184,10 +257,15 @@ class SemanticsNodeUpdateObservation {
required this.thickness,
required this.rect,
required this.label,
required this.hint,
this.labelAttributes,
required this.value,
this.valueAttributes,
required this.increasedValue,
this.increasedValueAttributes,
required this.decreasedValue,
this.decreasedValueAttributes,
required this.hint,
this.hintAttributes,
this.textDirection,
required this.transform,
required this.childrenInTraversalOrder,
......@@ -212,10 +290,15 @@ class SemanticsNodeUpdateObservation {
final double thickness;
final Rect rect;
final String label;
final String hint;
final List<ui.StringAttribute>? labelAttributes;
final String value;
final List<ui.StringAttribute>? valueAttributes;
final String increasedValue;
final List<ui.StringAttribute>? increasedValueAttributes;
final String decreasedValue;
final List<ui.StringAttribute>? decreasedValueAttributes;
final String hint;
final List<ui.StringAttribute>? hintAttributes;
final TextDirection? textDirection;
final Float64List transform;
final Int32List childrenInTraversalOrder;
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
......@@ -211,6 +212,152 @@ void main() {
});
});
group('Semantics', () {
testWidgets('Semantics can set attributed Text', (WidgetTester tester) async {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Semantics(
key: key,
attributedLabel: AttributedString(
'label',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
],
),
attributedValue: AttributedString(
'value',
attributes: <StringAttribute>[
LocaleStringAttribute(range: const TextRange(start: 0, end: 5), locale: const Locale('en', 'MX')),
],
),
attributedHint: AttributedString(
'hint',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)),
],
),
child: const Placeholder(),
)
),
)
);
final AttributedString attributedLabel = tester.getSemantics(find.byKey(key)).attributedLabel;
expect(attributedLabel.string, 'label');
expect(attributedLabel.attributes.length, 1);
expect(attributedLabel.attributes[0] is SpellOutStringAttribute, isTrue);
expect(attributedLabel.attributes[0].range, const TextRange(start:0, end: 5));
final AttributedString attributedValue = tester.getSemantics(find.byKey(key)).attributedValue;
expect(attributedValue.string, 'value');
expect(attributedValue.attributes.length, 1);
expect(attributedValue.attributes[0] is LocaleStringAttribute, isTrue);
final LocaleStringAttribute valueLocale = attributedValue.attributes[0] as LocaleStringAttribute;
expect(valueLocale.range, const TextRange(start:0, end: 5));
expect(valueLocale.locale, const Locale('en', 'MX'));
final AttributedString attributedHint = tester.getSemantics(find.byKey(key)).attributedHint;
expect(attributedHint.string, 'hint');
expect(attributedHint.attributes.length, 1);
expect(attributedHint.attributes[0] is SpellOutStringAttribute, isTrue);
expect(attributedHint.attributes[0].range, const TextRange(start:1, end: 2));
});
testWidgets('Semantics can merge attributed strings', (WidgetTester tester) async {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Semantics(
key: key,
attributedLabel: AttributedString(
'label',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
],
),
attributedHint: AttributedString(
'hint',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)),
],
),
child: Semantics(
attributedLabel: AttributedString(
'label',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
],
),
attributedHint: AttributedString(
'hint',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)),
],
),
child: const Placeholder(),
)
)
),
)
);
final AttributedString attributedLabel = tester.getSemantics(find.byKey(key)).attributedLabel;
expect(attributedLabel.string, 'label\nlabel');
expect(attributedLabel.attributes.length, 2);
expect(attributedLabel.attributes[0] is SpellOutStringAttribute, isTrue);
expect(attributedLabel.attributes[0].range, const TextRange(start:0, end: 5));
expect(attributedLabel.attributes[1] is SpellOutStringAttribute, isTrue);
expect(attributedLabel.attributes[1].range, const TextRange(start:6, end: 11));
final AttributedString attributedHint = tester.getSemantics(find.byKey(key)).attributedHint;
expect(attributedHint.string, 'hint\nhint');
expect(attributedHint.attributes.length, 2);
expect(attributedHint.attributes[0] is SpellOutStringAttribute, isTrue);
expect(attributedHint.attributes[0].range, const TextRange(start:1, end: 2));
expect(attributedHint.attributes[1] is SpellOutStringAttribute, isTrue);
expect(attributedHint.attributes[1].range, const TextRange(start:6, end: 7));
});
testWidgets('Semantics can merge attributed strings with non attributed string', (WidgetTester tester) async {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Semantics(
key: key,
attributedLabel: AttributedString(
'label1',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
],
),
child: Semantics(
label: 'label2',
child: Semantics(
attributedLabel: AttributedString(
'label3',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 1, end: 3)),
],
),
child: const Placeholder(),
),
)
)
),
)
);
final AttributedString attributedLabel = tester.getSemantics(find.byKey(key)).attributedLabel;
expect(attributedLabel.string, 'label1\nlabel2\nlabel3');
expect(attributedLabel.attributes.length, 2);
expect(attributedLabel.attributes[0] is SpellOutStringAttribute, isTrue);
expect(attributedLabel.attributes[0].range, const TextRange(start:0, end: 5));
expect(attributedLabel.attributes[1] is SpellOutStringAttribute, isTrue);
expect(attributedLabel.attributes[1].range, const TextRange(start:15, end: 17));
});
});
group('Row', () {
testWidgets('multiple baseline aligned children', (WidgetTester tester) async {
final UniqueKey key1 = UniqueKey();
......
......@@ -546,11 +546,11 @@ void main() {
final SemanticsData data = SemanticsData(
flags: flags,
actions: actions,
label: 'a',
increasedValue: 'b',
value: 'c',
decreasedValue: 'd',
hint: 'e',
attributedLabel: AttributedString('a'),
attributedIncreasedValue: AttributedString('b'),
attributedValue: AttributedString('c'),
attributedDecreasedValue: AttributedString('d'),
attributedHint: AttributedString('e'),
textDirection: TextDirection.ltr,
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
elevation: 3.0,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment