Unverified Commit 67cee630 authored by chunhtai's avatar chunhtai Committed by GitHub

Add string attribute api to text span (#86667)

parent 3ca8d7b7
......@@ -3,7 +3,7 @@
// found in the LICENSE file.
import 'dart:ui' as ui show ParagraphBuilder;
import 'dart:ui' as ui show ParagraphBuilder, StringAttribute;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
......@@ -56,6 +56,7 @@ class InlineSpanSemanticsInformation {
this.text, {
this.isPlaceholder = false,
this.semanticsLabel,
this.stringAttributes = const <ui.StringAttribute>[],
this.recognizer,
}) : assert(text != null),
assert(isPlaceholder != null),
......@@ -84,13 +85,17 @@ class InlineSpanSemanticsInformation {
/// [isPlaceholder] is true.
final bool requiresOwnNode;
/// The string attributes attached to this semantics information
final List<ui.StringAttribute> stringAttributes;
@override
bool operator ==(Object other) {
return other is InlineSpanSemanticsInformation
&& other.text == text
&& other.semanticsLabel == semanticsLabel
&& other.recognizer == recognizer
&& other.isPlaceholder == isPlaceholder;
&& other.isPlaceholder == isPlaceholder
&& listEquals<ui.StringAttribute>(other.stringAttributes, stringAttributes);
}
@override
......@@ -107,31 +112,40 @@ class InlineSpanSemanticsInformation {
List<InlineSpanSemanticsInformation> combineSemanticsInfo(List<InlineSpanSemanticsInformation> infoList) {
final List<InlineSpanSemanticsInformation> combined = <InlineSpanSemanticsInformation>[];
String workingText = '';
// TODO(ianh): this algorithm is internally inconsistent. workingText
// never becomes null, but we check for it being so below.
String? workingLabel;
String workingLabel = '';
List<ui.StringAttribute> workingAttributes = <ui.StringAttribute>[];
for (final InlineSpanSemanticsInformation info in infoList) {
if (info.requiresOwnNode) {
combined.add(InlineSpanSemanticsInformation(
workingText,
semanticsLabel: workingLabel ?? workingText,
semanticsLabel: workingLabel,
stringAttributes: workingAttributes,
));
workingText = '';
workingLabel = null;
workingLabel = '';
workingAttributes = <ui.StringAttribute>[];
combined.add(info);
} else {
workingText += info.text;
workingLabel ??= '';
if (info.semanticsLabel != null) {
workingLabel += info.semanticsLabel!;
} else {
workingLabel += info.text;
final String effectiveLabel = info.semanticsLabel ?? info.text;
for (final ui.StringAttribute infoAttribute in info.stringAttributes) {
workingAttributes.add(
infoAttribute.copy(
range: TextRange(
start: infoAttribute.range.start + workingLabel.length,
end: infoAttribute.range.end + workingLabel.length,
),
),
);
}
workingLabel += effectiveLabel;
}
}
combined.add(InlineSpanSemanticsInformation(
workingText,
semanticsLabel: workingLabel,
stringAttributes: workingAttributes,
));
return combined;
}
......
......@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui show ParagraphBuilder;
import 'dart:ui' as ui show ParagraphBuilder, Locale, StringAttribute, LocaleStringAttribute, SpellOutStringAttribute;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
......@@ -74,6 +74,8 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
this.onEnter,
this.onExit,
this.semanticsLabel,
this.locale,
this.spellOut,
}) : mouseCursor = mouseCursor ??
(recognizer == null ? MouseCursor.defer : SystemMouseCursors.click),
assert(!(text == null && semanticsLabel != null)),
......@@ -218,6 +220,32 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
/// ```
final String? semanticsLabel;
/// The language of the text in this span and its span children.
///
/// Setting the locale of this text span affects the way that assistive
/// technologies, such as VoiceOver or TalkBack, pronounce the text.
///
/// If this span contains other text span children, they also inherit the
/// locale from this span unless explicitly set to different locales.
final ui.Locale? locale;
/// Whether the assistive technologies should spell out this text character
/// by character.
///
/// If the text is 'hello world', setting this to true causes the assistive
/// technologies, such as VoiceOver or TalkBack, to pronounce
/// 'h-e-l-l-o-space-w-o-r-l-d' instead of complete words. This is useful for
/// texts, such as passwords or verification codes.
///
/// If this span contains other text span children, they also inherit the
/// property from this span unless explicitly set.
///
/// If the property is not set, this text span inherits the spell out setting
/// from its parent. If this text span does not have a parent or the parent
/// does not have a spell out setting, this text span does not spell out the
/// text by default.
final bool? spellOut;
@override
bool get validForMouseTracker => true;
......@@ -333,21 +361,42 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
}
@override
void computeSemanticsInformation(List<InlineSpanSemanticsInformation> collector) {
void computeSemanticsInformation(
List<InlineSpanSemanticsInformation> collector, {
ui.Locale? inheritedLocale,
bool inheritedSpellOut = false,
}) {
assert(debugAssertIsValid());
final ui.Locale? effectiveLocale = locale ?? inheritedLocale;
final bool effectiveSpellOut = spellOut ?? inheritedSpellOut;
if (text != null) {
collector.add(InlineSpanSemanticsInformation(
text!,
stringAttributes: <ui.StringAttribute>[
if (effectiveSpellOut)
ui.SpellOutStringAttribute(range: TextRange(start: 0, end: semanticsLabel?.length ?? text!.length)),
if (effectiveLocale != null)
ui.LocaleStringAttribute(locale: effectiveLocale, range: TextRange(start: 0, end: semanticsLabel?.length ?? text!.length)),
],
semanticsLabel: semanticsLabel,
recognizer: recognizer,
));
}
if (children != null) {
for (final InlineSpan child in children!) {
if (child is TextSpan) {
child.computeSemanticsInformation(
collector,
inheritedLocale: effectiveLocale,
inheritedSpellOut: effectiveSpellOut,
);
} else {
child.computeSemanticsInformation(collector);
}
}
}
}
@override
int? codeUnitAtVisitor(int index, Accumulator offset) {
......
......@@ -2274,11 +2274,15 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
/// The text to display.
InlineSpan? get text => _textPainter.text;
final TextPainter _textPainter;
AttributedString? _cachedAttributedValue;
List<InlineSpanSemanticsInformation>? _cachedCombinedSemanticsInfos;
set text(InlineSpan? value) {
if (_textPainter.text == value)
return;
_textPainter.text = value;
_cachedPlainText = null;
_cachedAttributedValue = null;
_cachedCombinedSemanticsInfos = null;
_extractPlaceholderSpans(value);
markNeedsTextLayout();
markNeedsSemanticsUpdate();
......@@ -2739,10 +2743,31 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
..explicitChildNodes = true;
return;
}
if (_cachedAttributedValue == null) {
if (obscureText) {
_cachedAttributedValue = AttributedString(obscuringCharacter * _plainText.length);
} else {
final StringBuffer buffer = StringBuffer();
int offset = 0;
final List<StringAttribute> attributes = <StringAttribute>[];
for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
final String label = info.semanticsLabel ?? info.text;
for (final StringAttribute infoAttribute in info.stringAttributes) {
final TextRange originalRange = infoAttribute.range;
attributes.add(
infoAttribute.copy(
range: TextRange(start: offset + originalRange.start, end: offset + originalRange.end),
),
);
}
buffer.write(label);
offset += label.length;
}
_cachedAttributedValue = AttributedString(buffer.toString(), attributes: attributes);
}
}
config
..value = obscureText
? obscuringCharacter * _plainText.length
: _plainText
..attributedValue = _cachedAttributedValue!
..isObscured = obscureText
..isMultiline = _isMultiline
..textDirection = textDirection
......@@ -2793,7 +2818,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
int childIndex = 0;
RenderBox? child = firstChild;
final Queue<SemanticsNode> newChildCache = Queue<SemanticsNode>();
for (final InlineSpanSemanticsInformation info in combineSemanticsInfo(_semanticsInfo!)) {
_cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) {
final TextSelection selection = TextSelection(
baseOffset: start,
extentOffset: start + info.text.length,
......@@ -2849,7 +2875,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
final SemanticsConfiguration configuration = SemanticsConfiguration()
..sortKey = OrdinalSortKey(ordinal++)
..textDirection = initialDirection
..label = info.semanticsLabel ?? info.text;
..attributedLabel = AttributedString(info.semanticsLabel ?? info.text, attributes: info.stringAttributes);
final GestureRecognizer? recognizer = info.recognizer;
if (recognizer != null) {
if (recognizer is TapGestureRecognizer) {
......
......@@ -118,6 +118,8 @@ class RenderParagraph extends RenderBox
}
final TextPainter _textPainter;
AttributedString? _cachedAttributedLabel;
List<InlineSpanSemanticsInformation>? _cachedCombinedSemanticsInfos;
/// The text to display.
InlineSpan get text => _textPainter.text!;
......@@ -129,6 +131,8 @@ class RenderParagraph extends RenderBox
return;
case RenderComparison.paint:
_textPainter.text = value;
_cachedAttributedLabel = null;
_cachedCombinedSemanticsInfos = null;
_extractPlaceholderSpans(value);
markNeedsPaint();
markNeedsSemanticsUpdate();
......@@ -136,6 +140,8 @@ class RenderParagraph extends RenderBox
case RenderComparison.layout:
_textPainter.text = value;
_overflowShader = null;
_cachedAttributedLabel = null;
_cachedCombinedSemanticsInfos = null;
_extractPlaceholderSpans(value);
markNeedsLayout();
break;
......@@ -869,11 +875,27 @@ class RenderParagraph extends RenderBox
config.explicitChildNodes = true;
config.isSemanticBoundary = true;
} else {
if (_cachedAttributedLabel == null) {
final StringBuffer buffer = StringBuffer();
int offset = 0;
final List<StringAttribute> attributes = <StringAttribute>[];
for (final InlineSpanSemanticsInformation info in _semanticsInfo!) {
buffer.write(info.semanticsLabel ?? info.text);
final String label = info.semanticsLabel ?? info.text;
for (final StringAttribute infoAttribute in info.stringAttributes) {
final TextRange originalRange = infoAttribute.range;
attributes.add(
infoAttribute.copy(
range: TextRange(start: offset + originalRange.start,
end: offset + originalRange.end)
),
);
}
buffer.write(label);
offset += label.length;
}
_cachedAttributedLabel = AttributedString(buffer.toString(), attributes: attributes);
}
config.label = buffer.toString();
config.attributedLabel = _cachedAttributedLabel!;
config.textDirection = textDirection;
}
}
......@@ -896,7 +918,8 @@ class RenderParagraph extends RenderBox
int childIndex = 0;
RenderBox? child = firstChild;
final Queue<SemanticsNode> newChildCache = Queue<SemanticsNode>();
for (final InlineSpanSemanticsInformation info in combineSemanticsInfo(_semanticsInfo!)) {
_cachedCombinedSemanticsInfos ??= combineSemanticsInfo(_semanticsInfo!);
for (final InlineSpanSemanticsInformation info in _cachedCombinedSemanticsInfos!) {
final TextSelection selection = TextSelection(
baseOffset: start,
extentOffset: start + info.text.length,
......@@ -952,7 +975,7 @@ class RenderParagraph extends RenderBox
final SemanticsConfiguration configuration = SemanticsConfiguration()
..sortKey = OrdinalSortKey(ordinal++)
..textDirection = initialDirection
..label = info.semanticsLabel ?? info.text;
..attributedLabel = AttributedString(info.semanticsLabel ?? info.text, attributes: info.stringAttributes);
final GestureRecognizer? recognizer = info.recognizer;
if (recognizer != null) {
if (recognizer is TapGestureRecognizer) {
......
......@@ -364,4 +364,53 @@ void main() {
expect(logEvents[1], isA<PointerExitEvent>());
});
testWidgets('TextSpan can compute StringAttributes', (WidgetTester tester) async {
const TextSpan span = TextSpan(
text: 'aaaaa',
spellOut: true,
children: <InlineSpan>[
TextSpan(text: 'yyyyy', locale: Locale('es', 'MX')),
TextSpan(
text: 'xxxxx',
spellOut: false,
children: <InlineSpan>[
TextSpan(text: 'zzzzz'),
TextSpan(text: 'bbbbb', spellOut: true),
]
),
],
);
final List<InlineSpanSemanticsInformation> collector = <InlineSpanSemanticsInformation>[];
span.computeSemanticsInformation(collector);
expect(collector.length, 5);
expect(collector[0].stringAttributes.length, 1);
expect(collector[0].stringAttributes[0], isA<SpellOutStringAttribute>());
expect(collector[0].stringAttributes[0].range, const TextRange(start: 0, end: 5));
expect(collector[1].stringAttributes.length, 2);
expect(collector[1].stringAttributes[0], isA<SpellOutStringAttribute>());
expect(collector[1].stringAttributes[0].range, const TextRange(start: 0, end: 5));
expect(collector[1].stringAttributes[1], isA<LocaleStringAttribute>());
expect(collector[1].stringAttributes[1].range, const TextRange(start: 0, end: 5));
final LocaleStringAttribute localeStringAttribute = collector[1].stringAttributes[1] as LocaleStringAttribute;
expect(localeStringAttribute.locale, const Locale('es', 'MX'));
expect(collector[2].stringAttributes.length, 0);
expect(collector[3].stringAttributes.length, 0);
expect(collector[4].stringAttributes.length, 1);
expect(collector[4].stringAttributes[0], isA<SpellOutStringAttribute>());
expect(collector[4].stringAttributes[0].range, const TextRange(start: 0, end: 5));
final List<InlineSpanSemanticsInformation> combined = combineSemanticsInfo(collector);
expect(combined.length, 1);
expect(combined[0].stringAttributes.length, 4);
expect(combined[0].stringAttributes[0], isA<SpellOutStringAttribute>());
expect(combined[0].stringAttributes[0].range, const TextRange(start: 0, end: 5));
expect(combined[0].stringAttributes[1], isA<SpellOutStringAttribute>());
expect(combined[0].stringAttributes[1].range, const TextRange(start: 5, end: 10));
expect(combined[0].stringAttributes[2], isA<LocaleStringAttribute>());
expect(combined[0].stringAttributes[2].range, const TextRange(start: 5, end: 10));
final LocaleStringAttribute combinedLocaleStringAttribute = combined[0].stringAttributes[2] as LocaleStringAttribute;
expect(combinedLocaleStringAttribute.locale, const Locale('es', 'MX'));
expect(combined[0].stringAttributes[3], isA<SpellOutStringAttribute>());
expect(combined[0].stringAttributes[3].range, const TextRange(start: 20, end: 25));
});
}
......@@ -48,8 +48,105 @@ void main() {
));
});
testWidgets('TextSpan Locale works', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: RichText(
text: TextSpan(
text: 'root',
locale: const Locale('es', 'MX'),
children: <InlineSpan>[
TextSpan(text: 'one', recognizer: TapGestureRecognizer()),
const WidgetSpan(
child: SizedBox(),
),
TextSpan(text: 'three', recognizer: DoubleTapGestureRecognizer()),
]
),
),
),
);
expect(tester.getSemantics(find.byType(RichText)), matchesSemantics(
children: <Matcher>[
matchesSemantics(
attributedLabel: AttributedString(
'root',
attributes: <StringAttribute>[
LocaleStringAttribute(range: const TextRange(start: 0, end: 4), locale: const Locale('es', 'MX')),
]
),
),
matchesSemantics(
attributedLabel: AttributedString(
'one',
attributes: <StringAttribute>[
LocaleStringAttribute(range: const TextRange(start: 0, end: 3), locale: const Locale('es', 'MX')),
]
),
),
matchesSemantics(
attributedLabel: AttributedString(
'three',
attributes: <StringAttribute>[
LocaleStringAttribute(range: const TextRange(start: 0, end: 5), locale: const Locale('es', 'MX')),
]
),
),
],
));
});
testWidgets('TextSpan spellOut works', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: RichText(
text: TextSpan(
text: 'root',
spellOut: true,
children: <InlineSpan>[
TextSpan(text: 'one', recognizer: TapGestureRecognizer()),
const WidgetSpan(
child: SizedBox(),
),
TextSpan(text: 'three', recognizer: DoubleTapGestureRecognizer()),
]
),
),
),
);
expect(tester.getSemantics(find.byType(RichText)), matchesSemantics(
children: <Matcher>[
matchesSemantics(
attributedLabel: AttributedString(
'root',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end: 4)),
]
),
),
matchesSemantics(
attributedLabel: AttributedString(
'one',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end: 3)),
]
),
),
matchesSemantics(
attributedLabel: AttributedString(
'three',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end: 5)),
]
),
),
],
));
});
testWidgets('WidgetSpan calculate correct intrinsic heights', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/48679.
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
......
......@@ -1488,6 +1488,72 @@ void main() {
semantics.dispose();
});
testWidgets('Selectable text rich text with spell out in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: SelectableText.rich(TextSpan(text: 'some text', spellOut: true)),
),
),
),
);
expect(
semantics,
includesNodeWith(
attributedValue: AttributedString(
'some text',
attributes: <StringAttribute>[
SpellOutStringAttribute(range: const TextRange(start: 0, end:9)),
],
),
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isReadOnly,
SemanticsFlag.isMultiline,
],
),
);
semantics.dispose();
});
testWidgets('Selectable text rich text with locale in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: SelectableText.rich(TextSpan(text: 'some text', locale: Locale('es', 'MX'))),
),
),
),
);
expect(
semantics,
includesNodeWith(
attributedValue: AttributedString(
'some text',
attributes: <StringAttribute>[
LocaleStringAttribute(range: const TextRange(start: 0, end:9), locale: const Locale('es', 'MX')),
],
),
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isReadOnly,
SemanticsFlag.isMultiline,
],
),
);
semantics.dispose();
});
testWidgets('Selectable rich text with gesture recognizer has correct semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
......
......@@ -427,6 +427,25 @@ class SemanticsTester {
@override
String toString() => 'SemanticsTester for ${tester.binding.pipelineOwner.semanticsOwner?.rootSemanticsNode}';
bool _stringAttributesEqual(List<StringAttribute> first, List<StringAttribute> second) {
if (first.length != second.length)
return false;
for (int i = 0; i < first.length; i++) {
if (first[i] is SpellOutStringAttribute &&
(second[i] is! SpellOutStringAttribute ||
second[i].range != first[i].range)) {
return false;
}
if (first[i] is LocaleStringAttribute &&
(second[i] is! LocaleStringAttribute ||
second[i].range != first[i].range ||
(second[i] as LocaleStringAttribute).locale != (second[i] as LocaleStringAttribute).locale)) {
return false;
}
}
return true;
}
/// Returns all semantics nodes in the current semantics tree whose properties
/// match the non-null arguments.
///
......@@ -435,6 +454,9 @@ class SemanticsTester {
///
/// If `ancestor` is not null, only the descendants of it are returned.
Iterable<SemanticsNode> nodesWith({
AttributedString? attributedLabel,
AttributedString? attributedValue,
AttributedString? attributedHint,
String? label,
String? value,
String? hint,
......@@ -451,10 +473,25 @@ class SemanticsTester {
bool checkNode(SemanticsNode node) {
if (label != null && node.label != label)
return false;
if (attributedLabel != null &&
(attributedLabel.string != node.attributedLabel.string ||
!_stringAttributesEqual(attributedLabel.attributes, node.attributedLabel.attributes))) {
return false;
}
if (value != null && node.value != value)
return false;
if (attributedValue != null &&
(attributedValue.string != node.attributedValue.string ||
!_stringAttributesEqual(attributedValue.attributes, node.attributedValue.attributes))) {
return false;
}
if (hint != null && node.hint != hint)
return false;
if (attributedHint != null &&
(attributedHint.string != node.attributedHint.string ||
!_stringAttributesEqual(attributedHint.attributes, node.attributedHint.attributes))) {
return false;
}
if (textDirection != null && node.textDirection != textDirection)
return false;
if (actions != null) {
......@@ -714,6 +751,9 @@ Matcher hasSemantics(
class _IncludesNodeWith extends Matcher {
const _IncludesNodeWith({
this.attributedLabel,
this.attributedValue,
this.attributedHint,
this.label,
this.value,
this.hint,
......@@ -725,7 +765,7 @@ class _IncludesNodeWith extends Matcher {
this.scrollExtentMin,
this.maxValueLength,
this.currentValueLength,
}) : assert(
}) : assert(
label != null ||
value != null ||
actions != null ||
......@@ -736,7 +776,9 @@ class _IncludesNodeWith extends Matcher {
maxValueLength != null ||
currentValueLength != null,
);
final AttributedString? attributedLabel;
final AttributedString? attributedValue;
final AttributedString? attributedHint;
final String? label;
final String? value;
final String? hint;
......@@ -752,6 +794,9 @@ class _IncludesNodeWith extends Matcher {
@override
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
return item.nodesWith(
attributedLabel: attributedLabel,
attributedValue: attributedValue,
attributedHint: attributedHint,
label: label,
value: value,
hint: hint,
......@@ -800,8 +845,11 @@ class _IncludesNodeWith extends Matcher {
/// If null is provided for an argument, it will match against any value.
Matcher includesNodeWith({
String? label,
AttributedString? attributedLabel,
String? value,
AttributedString? attributedValue,
String? hint,
AttributedString? attributedHint,
TextDirection? textDirection,
List<SemanticsAction>? actions,
List<SemanticsFlag>? flags,
......@@ -813,8 +861,11 @@ Matcher includesNodeWith({
}) {
return _IncludesNodeWith(
label: label,
attributedLabel: attributedLabel,
value: value,
attributedValue: attributedValue,
hint: hint,
attributedHint: attributedHint,
textDirection: textDirection,
actions: actions,
flags: flags,
......
......@@ -431,10 +431,15 @@ AsyncMatcher matchesReferenceImage(ui.Image image) {
/// * [WidgetTester.getSemantics], the tester method which retrieves semantics.
Matcher matchesSemantics({
String? label,
AttributedString? attributedLabel,
String? hint,
AttributedString? attributedHint,
String? value,
AttributedString? attributedValue,
String? increasedValue,
AttributedString? attributedIncreasedValue,
String? decreasedValue,
AttributedString? attributedDecreasedValue,
TextDirection? textDirection,
Rect? rect,
Size? size,
......@@ -559,10 +564,15 @@ Matcher matchesSemantics({
return _MatchesSemanticsData(
label: label,
attributedLabel: attributedLabel,
hint: hint,
attributedHint: attributedHint,
value: value,
attributedValue: attributedValue,
increasedValue: increasedValue,
attributedIncreasedValue: attributedIncreasedValue,
decreasedValue: decreasedValue,
attributedDecreasedValue: attributedDecreasedValue,
actions: actions,
flags: flags,
textDirection: textDirection,
......@@ -1708,10 +1718,15 @@ class _MatchesReferenceImage extends AsyncMatcher {
class _MatchesSemanticsData extends Matcher {
_MatchesSemanticsData({
this.label,
this.attributedLabel,
this.hint,
this.attributedHint,
this.value,
this.attributedValue,
this.increasedValue,
this.attributedIncreasedValue,
this.decreasedValue,
this.hint,
this.attributedDecreasedValue,
this.flags,
this.actions,
this.textDirection,
......@@ -1728,10 +1743,15 @@ class _MatchesSemanticsData extends Matcher {
});
final String? label;
final String? value;
final AttributedString? attributedLabel;
final String? hint;
final AttributedString? attributedHint;
final String? value;
final AttributedString? attributedValue;
final String? increasedValue;
final AttributedString? attributedIncreasedValue;
final String? decreasedValue;
final AttributedString? attributedDecreasedValue;
final SemanticsHintOverrides? hintOverrides;
final List<SemanticsAction>? actions;
final List<CustomSemanticsAction>? customActions;
......@@ -1751,14 +1771,24 @@ class _MatchesSemanticsData extends Matcher {
description.add('has semantics');
if (label != null)
description.add(' with label: $label');
if (attributedLabel != null)
description.add(' with attributedLabel: $attributedLabel');
if (value != null)
description.add(' with value: $value');
if (attributedValue != null)
description.add(' with attributedValue: $attributedValue');
if (hint != null)
description.add(' with hint: $hint');
if (attributedHint != null)
description.add(' with attributedHint: $attributedHint');
if (increasedValue != null)
description.add(' with increasedValue: $increasedValue ');
if (attributedIncreasedValue != null)
description.add(' with attributedIncreasedValue: $attributedIncreasedValue');
if (decreasedValue != null)
description.add(' with decreasedValue: $decreasedValue ');
if (attributedDecreasedValue != null)
description.add(' with attributedDecreasedValue: $attributedDecreasedValue');
if (actions != null)
description.add(' with actions: ').addDescriptionOf(actions);
if (flags != null)
......@@ -1791,6 +1821,24 @@ class _MatchesSemanticsData extends Matcher {
return description;
}
bool _stringAttributesEqual(List<StringAttribute> first, List<StringAttribute> second) {
if (first.length != second.length)
return false;
for (int i = 0; i < first.length; i++) {
if (first[i] is SpellOutStringAttribute &&
(second[i] is! SpellOutStringAttribute ||
second[i].range != first[i].range)) {
return false;
}
if (first[i] is LocaleStringAttribute &&
(second[i] is! LocaleStringAttribute ||
second[i].range != first[i].range ||
(second[i] as LocaleStringAttribute).locale != (second[i] as LocaleStringAttribute).locale)) {
return false;
}
}
return true;
}
@override
bool matches(dynamic node, Map<dynamic, dynamic> matchState) {
......@@ -1801,14 +1849,44 @@ class _MatchesSemanticsData extends Matcher {
final SemanticsData data = node is SemanticsNode ? node.getSemanticsData() : (node as SemanticsData);
if (label != null && label != data.label)
return failWithDescription(matchState, 'label was: ${data.label}');
if (attributedLabel != null &&
(attributedLabel!.string != data.attributedLabel.string ||
!_stringAttributesEqual(attributedLabel!.attributes, data.attributedLabel.attributes))) {
return failWithDescription(
matchState, 'attributedLabel was: ${data.attributedLabel}');
}
if (hint != null && hint != data.hint)
return failWithDescription(matchState, 'hint was: ${data.hint}');
if (attributedHint != null &&
(attributedHint!.string != data.attributedHint.string ||
!_stringAttributesEqual(attributedHint!.attributes, data.attributedHint.attributes))) {
return failWithDescription(
matchState, 'attributedHint was: ${data.attributedHint}');
}
if (value != null && value != data.value)
return failWithDescription(matchState, 'value was: ${data.value}');
if (attributedValue != null &&
(attributedValue!.string != data.attributedValue.string ||
!_stringAttributesEqual(attributedValue!.attributes, data.attributedValue.attributes))) {
return failWithDescription(
matchState, 'attributedValue was: ${data.attributedValue}');
}
if (increasedValue != null && increasedValue != data.increasedValue)
return failWithDescription(matchState, 'increasedValue was: ${data.increasedValue}');
if (attributedIncreasedValue != null &&
(attributedIncreasedValue!.string != data.attributedIncreasedValue.string ||
!_stringAttributesEqual(attributedIncreasedValue!.attributes, data.attributedIncreasedValue.attributes))) {
return failWithDescription(
matchState, 'attributedIncreasedValue was: ${data.attributedIncreasedValue}');
}
if (decreasedValue != null && decreasedValue != data.decreasedValue)
return failWithDescription(matchState, 'decreasedValue was: ${data.decreasedValue}');
if (attributedDecreasedValue != null &&
(attributedDecreasedValue!.string != data.attributedDecreasedValue.string ||
!_stringAttributesEqual(attributedDecreasedValue!.attributes, data.attributedDecreasedValue.attributes))) {
return failWithDescription(
matchState, 'attributedDecreasedValue was: ${data.attributedDecreasedValue}');
}
if (textDirection != null && textDirection != data.textDirection)
return failWithDescription(matchState, 'textDirection was: $textDirection');
if (rect != null && rect != data.rect)
......
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