diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 5d6efc3fc17..7e1c94a6773 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -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) diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index 09cfd7a9646..26c3959d9ee 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -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 [], + }) : 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 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 newAttributes = List.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(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('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, 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('checked', checked, defaultValue: null)); properties.add(DiagnosticsProperty('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, defaultValue: null)); properties.add(DiagnosticsProperty('sortKey', sortKey, defaultValue: null)); properties.add(DiagnosticsProperty('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 []); 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? mergedTags = tags == null ? null : Set.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 ??= {}; 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('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, defaultValue: null)); properties.add(DiagnosticsProperty('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; } - /// A brief description of the result of performing an action on this node. + /// The value that [value] will have after performing a + /// [SemanticsAction.increase] action in [AttributedString] format. /// - /// 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. + /// 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 hint => _hint; - String _hint = ''; + 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. + /// + /// Setting this attribute will override the [attributedHint]. + /// + /// The reading direction is given by [textDirection]. + /// + /// 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 diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 36dba1a34eb..72e031355ac 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -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) diff --git a/packages/flutter/lib/src/widgets/semantics_debugger.dart b/packages/flutter/lib/src/widgets/semantics_debugger.dart index ddab81479f8..8a0817ae89b 100644 --- a/packages/flutter/lib/src/widgets/semantics_debugger.dart +++ b/packages/flutter/lib/src/widgets/semantics_debugger.dart @@ -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; } } diff --git a/packages/flutter/test/rendering/reattach_test.dart b/packages/flutter/test/rendering/reattach_test.dart index 587a3116dbe..74c13216a0d 100644 --- a/packages/flutter/test/rendering/reattach_test.dart +++ b/packages/flutter/test/rendering/reattach_test.dart @@ -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), ), ), ), diff --git a/packages/flutter/test/rendering/simple_semantics_test.dart b/packages/flutter/test/rendering/simple_semantics_test.dart index 735e393095d..1fae3fd442f 100644 --- a/packages/flutter/test/rendering/simple_semantics_test.dart +++ b/packages/flutter/test/rendering/simple_semantics_test.dart @@ -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); diff --git a/packages/flutter/test/semantics/semantics_test.dart b/packages/flutter/test/semantics/semantics_test.dart index 60066e6895b..64f1aec1c12 100644 --- a/packages/flutter/test/semantics/semantics_test.dart +++ b/packages/flutter/test/semantics/semantics_test.dart @@ -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: [ + 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: [ + 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: [ + 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: [ + SpellOutStringAttribute(range: const TextRange(start:0, end:4)), + ] + ); + final AttributedString string2 = AttributedString( + 'string2', + attributes: [ + 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; diff --git a/packages/flutter/test/semantics/semantics_update_test.dart b/packages/flutter/test/semantics/semantics_update_test.dart index b4f9d68c93c..d104d8f13b1 100644 --- a/packages/flutter/test/semantics/semantics_update_test.dart +++ b/packages/flutter/test/semantics/semantics_update_test.dart @@ -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: [ + SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)), + ], + ), + attributedValue: AttributedString( + 'value', + attributes: [ + LocaleStringAttribute(range: const TextRange(start: 0, end: 5), locale: const Locale('en', 'MX')), + ], + ), + attributedHint: AttributedString( + 'hint', + attributes: [ + 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 when engine - // pr lands: https://github.com/flutter/engine/pull/25373. - // https://github.com/flutter/flutter/issues/79318. - Object? labelAttributes, + List? labelAttributes, required String value, - Object? valueAttributes, + List? valueAttributes, required String increasedValue, - Object? increasedValueAttributes, + List? increasedValueAttributes, required String decreasedValue, - Object? decreasedValueAttributes, + List? decreasedValueAttributes, required String hint, - Object? hintAttributes, + List? 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? labelAttributes; final String value; + final List? valueAttributes; final String increasedValue; + final List? increasedValueAttributes; final String decreasedValue; + final List? decreasedValueAttributes; + final String hint; + final List? hintAttributes; final TextDirection? textDirection; final Float64List transform; final Int32List childrenInTraversalOrder; diff --git a/packages/flutter/test/widgets/basic_test.dart b/packages/flutter/test/widgets/basic_test.dart index 7e426167ce2..a9605166cc7 100644 --- a/packages/flutter/test/widgets/basic_test.dart +++ b/packages/flutter/test/widgets/basic_test.dart @@ -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: [ + SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)), + ], + ), + attributedValue: AttributedString( + 'value', + attributes: [ + LocaleStringAttribute(range: const TextRange(start: 0, end: 5), locale: const Locale('en', 'MX')), + ], + ), + attributedHint: AttributedString( + 'hint', + attributes: [ + 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: [ + SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)), + ], + ), + attributedHint: AttributedString( + 'hint', + attributes: [ + SpellOutStringAttribute(range: const TextRange(start: 1, end: 2)), + ], + ), + child: Semantics( + attributedLabel: AttributedString( + 'label', + attributes: [ + SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)), + ], + ), + attributedHint: AttributedString( + 'hint', + attributes: [ + 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: [ + SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)), + ], + ), + child: Semantics( + label: 'label2', + child: Semantics( + attributedLabel: AttributedString( + 'label3', + attributes: [ + 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(); diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index 86b7afb1deb..1c6782d01e3 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart @@ -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,