Unverified Commit 352ad3a9 authored by chunhtai's avatar chunhtai Committed by GitHub

Adds API in semanticsconfiguration to decide how to merge child semanticsConfigurations (#110730)

* Adds semantics merger API and fix input decorator

* addressing comments

* abstractnode to object

* feature complete

* addressing comments

* fix comments

* conditionally add sort order

* fix bool

* fix test

* more fix

* fix tests
parent 182f9f66
...@@ -1326,6 +1326,35 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin ...@@ -1326,6 +1326,35 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
return Size.zero; return Size.zero;
} }
ChildSemanticsConfigurationsResult _childSemanticsConfigurationDelegate(List<SemanticsConfiguration> childConfigs) {
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
List<SemanticsConfiguration>? prefixMergeGroup;
List<SemanticsConfiguration>? suffixMergeGroup;
for (final SemanticsConfiguration childConfig in childConfigs) {
if (childConfig.tagsChildrenWith(_InputDecoratorState._kPrefixSemanticsTag)) {
prefixMergeGroup ??= <SemanticsConfiguration>[];
prefixMergeGroup.add(childConfig);
} else if (childConfig.tagsChildrenWith(_InputDecoratorState._kSuffixSemanticsTag)) {
suffixMergeGroup ??= <SemanticsConfiguration>[];
suffixMergeGroup.add(childConfig);
} else {
builder.markAsMergeUp(childConfig);
}
}
if (prefixMergeGroup != null) {
builder.markAsSiblingMergeGroup(prefixMergeGroup);
}
if (suffixMergeGroup != null) {
builder.markAsSiblingMergeGroup(suffixMergeGroup);
}
return builder.build();
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
config.childConfigurationsDelegate = _childSemanticsConfigurationDelegate;
}
@override @override
void performLayout() { void performLayout() {
final BoxConstraints constraints = this.constraints; final BoxConstraints constraints = this.constraints;
...@@ -1713,12 +1742,16 @@ class _AffixText extends StatelessWidget { ...@@ -1713,12 +1742,16 @@ class _AffixText extends StatelessWidget {
this.text, this.text,
this.style, this.style,
this.child, this.child,
this.semanticsSortKey,
required this.semanticsTag,
}); });
final bool labelIsFloating; final bool labelIsFloating;
final String? text; final String? text;
final TextStyle? style; final TextStyle? style;
final Widget? child; final Widget? child;
final SemanticsSortKey? semanticsSortKey;
final SemanticsTag semanticsTag;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
...@@ -1728,8 +1761,12 @@ class _AffixText extends StatelessWidget { ...@@ -1728,8 +1761,12 @@ class _AffixText extends StatelessWidget {
duration: _kTransitionDuration, duration: _kTransitionDuration,
curve: _kTransitionCurve, curve: _kTransitionCurve,
opacity: labelIsFloating ? 1.0 : 0.0, opacity: labelIsFloating ? 1.0 : 0.0,
child: Semantics(
sortKey: semanticsSortKey,
tagForChildren: semanticsTag,
child: child ?? (text == null ? null : Text(text!, style: style)), child: child ?? (text == null ? null : Text(text!, style: style)),
), ),
),
); );
} }
} }
...@@ -1899,6 +1936,11 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -1899,6 +1936,11 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
late AnimationController _floatingLabelController; late AnimationController _floatingLabelController;
late AnimationController _shakingLabelController; late AnimationController _shakingLabelController;
final _InputBorderGap _borderGap = _InputBorderGap(); final _InputBorderGap _borderGap = _InputBorderGap();
static const OrdinalSortKey _kPrefixSemanticsSortOrder = OrdinalSortKey(0);
static const OrdinalSortKey _kInputSemanticsSortOrder = OrdinalSortKey(1);
static const OrdinalSortKey _kSuffixSemanticsSortOrder = OrdinalSortKey(2);
static const SemanticsTag _kPrefixSemanticsTag = SemanticsTag('_InputDecoratorState.prefix');
static const SemanticsTag _kSuffixSemanticsTag = SemanticsTag('_InputDecoratorState.suffix');
@override @override
void initState() { void initState() {
...@@ -2218,22 +2260,42 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -2218,22 +2260,42 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
), ),
); );
final Widget? prefix = decoration.prefix == null && decoration.prefixText == null ? null : final bool hasPrefix = decoration.prefix != null || decoration.prefixText != null;
_AffixText( final bool hasSuffix = decoration.suffix != null || decoration.suffixText != null;
Widget? input = widget.child;
// If at least two out of the three are visible, it needs semantics sort
// order.
final bool needsSemanticsSortOrder = widget._labelShouldWithdraw && (input != null ? (hasPrefix || hasSuffix) : (hasPrefix && hasSuffix));
final Widget? prefix = hasPrefix
? _AffixText(
labelIsFloating: widget._labelShouldWithdraw, labelIsFloating: widget._labelShouldWithdraw,
text: decoration.prefixText, text: decoration.prefixText,
style: MaterialStateProperty.resolveAs(decoration.prefixStyle, materialState) ?? hintStyle, style: MaterialStateProperty.resolveAs(decoration.prefixStyle, materialState) ?? hintStyle,
semanticsSortKey: needsSemanticsSortOrder ? _kPrefixSemanticsSortOrder : null,
semanticsTag: _kPrefixSemanticsTag,
child: decoration.prefix, child: decoration.prefix,
); )
: null;
final Widget? suffix = decoration.suffix == null && decoration.suffixText == null ? null : final Widget? suffix = hasSuffix
_AffixText( ? _AffixText(
labelIsFloating: widget._labelShouldWithdraw, labelIsFloating: widget._labelShouldWithdraw,
text: decoration.suffixText, text: decoration.suffixText,
style: MaterialStateProperty.resolveAs(decoration.suffixStyle, materialState) ?? hintStyle, style: MaterialStateProperty.resolveAs(decoration.suffixStyle, materialState) ?? hintStyle,
semanticsSortKey: needsSemanticsSortOrder ? _kSuffixSemanticsSortOrder : null,
semanticsTag: _kSuffixSemanticsTag,
child: decoration.suffix, child: decoration.suffix,
); )
: null;
if (input != null && needsSemanticsSortOrder) {
input = Semantics(
sortKey: _kInputSemanticsSortOrder,
child: input,
);
}
final bool decorationIsDense = decoration.isDense ?? false; final bool decorationIsDense = decoration.isDense ?? false;
final double iconSize = decorationIsDense ? 18.0 : 24.0; final double iconSize = decorationIsDense ? 18.0 : 24.0;
...@@ -2272,7 +2334,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -2272,7 +2334,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
color: _getPrefixIconColor(themeData, defaults), color: _getPrefixIconColor(themeData, defaults),
size: iconSize, size: iconSize,
), ),
child: decoration.prefixIcon!, child: Semantics(
child: decoration.prefixIcon,
),
), ),
), ),
), ),
...@@ -2297,7 +2361,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -2297,7 +2361,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
color: _getSuffixIconColor(themeData, defaults), color: _getSuffixIconColor(themeData, defaults),
size: iconSize, size: iconSize,
), ),
child: decoration.suffixIcon!, child: Semantics(
child: decoration.suffixIcon,
),
), ),
), ),
), ),
...@@ -2374,7 +2440,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -2374,7 +2440,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
isDense: decoration.isDense, isDense: decoration.isDense,
visualDensity: themeData.visualDensity, visualDensity: themeData.visualDensity,
icon: icon, icon: icon,
input: widget.child, input: input,
label: label, label: label,
hint: hint, hint: hint,
prefix: prefix, prefix: prefix,
......
...@@ -3100,6 +3100,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -3100,6 +3100,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
if (_cachedSemanticsConfiguration == null) { if (_cachedSemanticsConfiguration == null) {
_cachedSemanticsConfiguration = SemanticsConfiguration(); _cachedSemanticsConfiguration = SemanticsConfiguration();
describeSemanticsConfiguration(_cachedSemanticsConfiguration!); describeSemanticsConfiguration(_cachedSemanticsConfiguration!);
assert(
!_cachedSemanticsConfiguration!.explicitChildNodes || _cachedSemanticsConfiguration!.childConfigurationsDelegate == null,
'A SemanticsConfiguration with explicitChildNode set to true cannot have a non-null childConfigsDelegate.',
);
} }
return _cachedSemanticsConfiguration!; return _cachedSemanticsConfiguration!;
} }
...@@ -3161,7 +3165,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -3161,7 +3165,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
final bool wasSemanticsBoundary = _semantics != null && (_cachedSemanticsConfiguration?.isSemanticBoundary ?? false); final bool wasSemanticsBoundary = _semantics != null && (_cachedSemanticsConfiguration?.isSemanticBoundary ?? false);
_cachedSemanticsConfiguration = null; _cachedSemanticsConfiguration = null;
bool isEffectiveSemanticsBoundary = _semanticsConfiguration.isSemanticBoundary && wasSemanticsBoundary; // The childConfigurationsDelegate may produce sibling nodes to be attached
// to the parent of this semantics node, thus it can't be a semantics
// boundary.
bool isEffectiveSemanticsBoundary =
_semanticsConfiguration.childConfigurationsDelegate == null &&
_semanticsConfiguration.isSemanticBoundary &&
wasSemanticsBoundary;
RenderObject node = this; RenderObject node = this;
while (!isEffectiveSemanticsBoundary && node.parent is RenderObject) { while (!isEffectiveSemanticsBoundary && node.parent is RenderObject) {
...@@ -3213,11 +3223,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -3213,11 +3223,13 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
assert(fragment is _InterestingSemanticsFragment); assert(fragment is _InterestingSemanticsFragment);
final _InterestingSemanticsFragment interestingFragment = fragment as _InterestingSemanticsFragment; final _InterestingSemanticsFragment interestingFragment = fragment as _InterestingSemanticsFragment;
final List<SemanticsNode> result = <SemanticsNode>[]; final List<SemanticsNode> result = <SemanticsNode>[];
final List<SemanticsNode> siblingNodes = <SemanticsNode>[];
interestingFragment.compileChildren( interestingFragment.compileChildren(
parentSemanticsClipRect: _semantics?.parentSemanticsClipRect, parentSemanticsClipRect: _semantics?.parentSemanticsClipRect,
parentPaintClipRect: _semantics?.parentPaintClipRect, parentPaintClipRect: _semantics?.parentPaintClipRect,
elevationAdjustment: _semantics?.elevationAdjustment ?? 0.0, elevationAdjustment: _semantics?.elevationAdjustment ?? 0.0,
result: result, result: result,
siblingNodes: siblingNodes,
); );
final SemanticsNode node = result.single; final SemanticsNode node = result.single;
// Fragment only wants to add this node's SemanticsNode to the parent. // Fragment only wants to add this node's SemanticsNode to the parent.
...@@ -3235,70 +3247,94 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -3235,70 +3247,94 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
bool dropSemanticsOfPreviousSiblings = config.isBlockingSemanticsOfPreviouslyPaintedNodes; bool dropSemanticsOfPreviousSiblings = config.isBlockingSemanticsOfPreviouslyPaintedNodes;
final bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary; final bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
final List<_InterestingSemanticsFragment> fragments = <_InterestingSemanticsFragment>[];
final Set<_InterestingSemanticsFragment> toBeMarkedExplicit = <_InterestingSemanticsFragment>{};
final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants; final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants;
final List<SemanticsConfiguration> childConfigurations = <SemanticsConfiguration>[];
final bool explicitChildNode = config.explicitChildNodes || parent is! RenderObject;
final bool hasChildConfigurationsDelegate = config.childConfigurationsDelegate != null;
final Map<SemanticsConfiguration, _InterestingSemanticsFragment> configToFragment = <SemanticsConfiguration, _InterestingSemanticsFragment>{};
final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
final List<List<_InterestingSemanticsFragment>> siblingMergeFragmentGroups = <List<_InterestingSemanticsFragment>>[];
visitChildrenForSemantics((RenderObject renderChild) { visitChildrenForSemantics((RenderObject renderChild) {
assert(!_needsLayout); assert(!_needsLayout);
final _SemanticsFragment parentFragment = renderChild._getSemanticsForParent( final _SemanticsFragment parentFragment = renderChild._getSemanticsForParent(
mergeIntoParent: childrenMergeIntoParent, mergeIntoParent: childrenMergeIntoParent,
); );
if (parentFragment.dropsSemanticsOfPreviousSiblings) { if (parentFragment.dropsSemanticsOfPreviousSiblings) {
fragments.clear(); childConfigurations.clear();
toBeMarkedExplicit.clear(); mergeUpFragments.clear();
siblingMergeFragmentGroups.clear();
if (!config.isSemanticBoundary) { if (!config.isSemanticBoundary) {
dropSemanticsOfPreviousSiblings = true; dropSemanticsOfPreviousSiblings = true;
} }
} }
// Figure out which child fragments are to be made explicit. for (final _InterestingSemanticsFragment fragment in parentFragment.mergeUpFragments) {
for (final _InterestingSemanticsFragment fragment in parentFragment.interestingFragments) {
fragments.add(fragment);
fragment.addAncestor(this); fragment.addAncestor(this);
fragment.addTags(config.tagsForChildren); fragment.addTags(config.tagsForChildren);
if (config.explicitChildNodes || parent is! RenderObject) { if (hasChildConfigurationsDelegate && fragment.config != null) {
fragment.markAsExplicit(); // This fragment need to go through delegate to determine whether it
continue; // merge up or not.
} childConfigurations.add(fragment.config!);
if (!fragment.hasConfigForParent || producesForkingFragment) { configToFragment[fragment.config!] = fragment;
continue; } else {
mergeUpFragments.add(fragment);
} }
if (!config.isCompatibleWith(fragment.config)) {
toBeMarkedExplicit.add(fragment);
} }
final int siblingLength = fragments.length - 1; if (parentFragment is _ContainerSemanticsFragment) {
for (int i = 0; i < siblingLength; i += 1) { // Container fragments needs to propagate sibling merge group to be
final _InterestingSemanticsFragment siblingFragment = fragments[i]; // compiled by _SwitchableSemanticsFragment.
if (!fragment.config!.isCompatibleWith(siblingFragment.config)) { for (final List<_InterestingSemanticsFragment> siblingMergeGroup in parentFragment.siblingMergeGroups) {
toBeMarkedExplicit.add(fragment); for (final _InterestingSemanticsFragment siblingMergingFragment in siblingMergeGroup) {
toBeMarkedExplicit.add(siblingFragment); siblingMergingFragment.addAncestor(this);
siblingMergingFragment.addTags(config.tagsForChildren);
} }
siblingMergeFragmentGroups.add(siblingMergeGroup);
} }
} }
}); });
for (final _InterestingSemanticsFragment fragment in toBeMarkedExplicit) { assert(hasChildConfigurationsDelegate || configToFragment.isEmpty);
if (explicitChildNode) {
for (final _InterestingSemanticsFragment fragment in mergeUpFragments) {
fragment.markAsExplicit(); fragment.markAsExplicit();
} }
} else if (hasChildConfigurationsDelegate && childConfigurations.isNotEmpty) {
final ChildSemanticsConfigurationsResult result = config.childConfigurationsDelegate!(childConfigurations);
mergeUpFragments.addAll(
result.mergeUp.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!),
);
for (final Iterable<SemanticsConfiguration> group in result.siblingMergeGroups) {
siblingMergeFragmentGroups.add(
group.map<_InterestingSemanticsFragment>((SemanticsConfiguration config) => configToFragment[config]!).toList()
);
}
}
_needsSemanticsUpdate = false; _needsSemanticsUpdate = false;
_SemanticsFragment result; final _SemanticsFragment result;
if (parent is! RenderObject) { if (parent is! RenderObject) {
assert(!config.hasBeenAnnotated); assert(!config.hasBeenAnnotated);
assert(!mergeIntoParent); assert(!mergeIntoParent);
assert(siblingMergeFragmentGroups.isEmpty);
_marksExplicitInMergeGroup(mergeUpFragments, isMergeUp: true);
siblingMergeFragmentGroups.forEach(_marksExplicitInMergeGroup);
result = _RootSemanticsFragment( result = _RootSemanticsFragment(
owner: this, owner: this,
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
); );
} else if (producesForkingFragment) { } else if (producesForkingFragment) {
result = _ContainerSemanticsFragment( result = _ContainerSemanticsFragment(
siblingMergeGroups: siblingMergeFragmentGroups,
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
); );
} else { } else {
_marksExplicitInMergeGroup(mergeUpFragments, isMergeUp: true);
siblingMergeFragmentGroups.forEach(_marksExplicitInMergeGroup);
result = _SwitchableSemanticsFragment( result = _SwitchableSemanticsFragment(
config: config, config: config,
mergeIntoParent: mergeIntoParent, mergeIntoParent: mergeIntoParent,
siblingMergeGroups: siblingMergeFragmentGroups,
owner: this, owner: this,
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings, dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
); );
...@@ -3307,12 +3343,34 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -3307,12 +3343,34 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
fragment.markAsExplicit(); fragment.markAsExplicit();
} }
} }
result.addAll(mergeUpFragments);
result.addAll(fragments);
return result; return result;
} }
void _marksExplicitInMergeGroup(List<_InterestingSemanticsFragment> mergeGroup, {bool isMergeUp = false}) {
final Set<_InterestingSemanticsFragment> toBeExplicit = <_InterestingSemanticsFragment>{};
for (int i = 0; i < mergeGroup.length; i += 1) {
final _InterestingSemanticsFragment fragment = mergeGroup[i];
if (!fragment.hasConfigForParent) {
continue;
}
if (isMergeUp && !_semanticsConfiguration.isCompatibleWith(fragment.config)) {
toBeExplicit.add(fragment);
}
final int siblingLength = i;
for (int j = 0; j < siblingLength; j += 1) {
final _InterestingSemanticsFragment siblingFragment = mergeGroup[j];
if (!fragment.config!.isCompatibleWith(siblingFragment.config)) {
toBeExplicit.add(fragment);
toBeExplicit.add(siblingFragment);
}
}
}
for (final _InterestingSemanticsFragment fragment in toBeExplicit) {
fragment.markAsExplicit();
}
}
/// Called when collecting the semantics of this node. /// Called when collecting the semantics of this node.
/// ///
/// The implementation has to return the children in paint order skipping all /// The implementation has to return the children in paint order skipping all
...@@ -3985,8 +4043,9 @@ mixin RelayoutWhenSystemFontsChangeMixin on RenderObject { ...@@ -3985,8 +4043,9 @@ mixin RelayoutWhenSystemFontsChangeMixin on RenderObject {
/// * [_ContainerSemanticsFragment]: a container class to transport the semantic /// * [_ContainerSemanticsFragment]: a container class to transport the semantic
/// information of multiple [_InterestingSemanticsFragment] to a parent. /// information of multiple [_InterestingSemanticsFragment] to a parent.
abstract class _SemanticsFragment { abstract class _SemanticsFragment {
_SemanticsFragment({ required this.dropsSemanticsOfPreviousSiblings }) _SemanticsFragment({
: assert (dropsSemanticsOfPreviousSiblings != null); required this.dropsSemanticsOfPreviousSiblings,
}) : assert (dropsSemanticsOfPreviousSiblings != null);
/// Incorporate the fragments of children into this fragment. /// Incorporate the fragments of children into this fragment.
void addAll(Iterable<_InterestingSemanticsFragment> fragments); void addAll(Iterable<_InterestingSemanticsFragment> fragments);
...@@ -4002,25 +4061,29 @@ abstract class _SemanticsFragment { ...@@ -4002,25 +4061,29 @@ abstract class _SemanticsFragment {
/// Returns [_InterestingSemanticsFragment] describing the actual semantic /// Returns [_InterestingSemanticsFragment] describing the actual semantic
/// information that this fragment wants to add to the parent. /// information that this fragment wants to add to the parent.
List<_InterestingSemanticsFragment> get interestingFragments; List<_InterestingSemanticsFragment> get mergeUpFragments;
} }
/// A container used when a [RenderObject] wants to add multiple independent /// A container used when a [RenderObject] wants to add multiple independent
/// [_InterestingSemanticsFragment] to its parent. /// [_InterestingSemanticsFragment] to its parent.
/// ///
/// The [_InterestingSemanticsFragment] to be added to the parent can be /// The [_InterestingSemanticsFragment] to be added to the parent can be
/// obtained via [interestingFragments]. /// obtained via [mergeUpFragments].
class _ContainerSemanticsFragment extends _SemanticsFragment { class _ContainerSemanticsFragment extends _SemanticsFragment {
_ContainerSemanticsFragment({
required super.dropsSemanticsOfPreviousSiblings,
required this.siblingMergeGroups,
});
_ContainerSemanticsFragment({ required super.dropsSemanticsOfPreviousSiblings }); final List<List<_InterestingSemanticsFragment>> siblingMergeGroups;
@override @override
void addAll(Iterable<_InterestingSemanticsFragment> fragments) { void addAll(Iterable<_InterestingSemanticsFragment> fragments) {
interestingFragments.addAll(fragments); mergeUpFragments.addAll(fragments);
} }
@override @override
final List<_InterestingSemanticsFragment> interestingFragments = <_InterestingSemanticsFragment>[]; final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
} }
/// A [_SemanticsFragment] that describes which concrete semantic information /// A [_SemanticsFragment] that describes which concrete semantic information
...@@ -4057,6 +4120,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment { ...@@ -4057,6 +4120,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
required Rect? parentPaintClipRect, required Rect? parentPaintClipRect,
required double elevationAdjustment, required double elevationAdjustment,
required List<SemanticsNode> result, required List<SemanticsNode> result,
required List<SemanticsNode> siblingNodes,
}); });
/// The [SemanticsConfiguration] the child wants to merge into the parent's /// The [SemanticsConfiguration] the child wants to merge into the parent's
...@@ -4086,7 +4150,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment { ...@@ -4086,7 +4150,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
bool get hasConfigForParent => config != null; bool get hasConfigForParent => config != null;
@override @override
List<_InterestingSemanticsFragment> get interestingFragments => <_InterestingSemanticsFragment>[this]; List<_InterestingSemanticsFragment> get mergeUpFragments => <_InterestingSemanticsFragment>[this];
Set<SemanticsTag>? _tagsForChildren; Set<SemanticsTag>? _tagsForChildren;
...@@ -4124,7 +4188,13 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment { ...@@ -4124,7 +4188,13 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
}); });
@override @override
void compileChildren({ Rect? parentSemanticsClipRect, Rect? parentPaintClipRect, required double elevationAdjustment, required List<SemanticsNode> result }) { void compileChildren({
Rect? parentSemanticsClipRect,
Rect? parentPaintClipRect,
required double elevationAdjustment,
required List<SemanticsNode> result,
required List<SemanticsNode> siblingNodes,
}) {
assert(_tagsForChildren == null || _tagsForChildren!.isEmpty); assert(_tagsForChildren == null || _tagsForChildren!.isEmpty);
assert(parentSemanticsClipRect == null); assert(parentSemanticsClipRect == null);
assert(parentPaintClipRect == null); assert(parentPaintClipRect == null);
...@@ -4150,8 +4220,11 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment { ...@@ -4150,8 +4220,11 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
parentPaintClipRect: parentPaintClipRect, parentPaintClipRect: parentPaintClipRect,
elevationAdjustment: 0.0, elevationAdjustment: 0.0,
result: children, result: children,
siblingNodes: siblingNodes,
); );
} }
// Root node does not have a parent and thus can't attach sibling nodes.
assert(siblingNodes.isEmpty);
node.updateWith(config: null, childrenInInversePaintOrder: children); node.updateWith(config: null, childrenInInversePaintOrder: children);
// The root node is the only semantics node allowed to be invisible. This // The root node is the only semantics node allowed to be invisible. This
...@@ -4201,9 +4274,11 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment { ...@@ -4201,9 +4274,11 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
_SwitchableSemanticsFragment({ _SwitchableSemanticsFragment({
required bool mergeIntoParent, required bool mergeIntoParent,
required SemanticsConfiguration config, required SemanticsConfiguration config,
required List<List<_InterestingSemanticsFragment>> siblingMergeGroups,
required super.owner, required super.owner,
required super.dropsSemanticsOfPreviousSiblings, required super.dropsSemanticsOfPreviousSiblings,
}) : _mergeIntoParent = mergeIntoParent, }) : _siblingMergeGroups = siblingMergeGroups,
_mergeIntoParent = mergeIntoParent,
_config = config, _config = config,
assert(mergeIntoParent != null), assert(mergeIntoParent != null),
assert(config != null); assert(config != null);
...@@ -4211,14 +4286,126 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment { ...@@ -4211,14 +4286,126 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
final bool _mergeIntoParent; final bool _mergeIntoParent;
SemanticsConfiguration _config; SemanticsConfiguration _config;
bool _isConfigWritable = false; bool _isConfigWritable = false;
bool _mergesToSibling = false;
final List<List<_InterestingSemanticsFragment>> _siblingMergeGroups;
void _mergeSiblingGroup(Rect? parentSemanticsClipRect, Rect? parentPaintClipRect, List<SemanticsNode> result, Set<int> usedSemanticsIds) {
for (final List<_InterestingSemanticsFragment> group in _siblingMergeGroups) {
Rect? rect;
Rect? semanticsClipRect;
Rect? paintClipRect;
SemanticsConfiguration? configuration;
// Use empty set because the _tagsForChildren may not contains all of the
// tags if this fragment is not explicit. The _tagsForChildren are added
// to sibling nodes at the end of compileChildren if this fragment is
// explicit.
final Set<SemanticsTag> tags = <SemanticsTag>{};
SemanticsNode? node;
for (final _InterestingSemanticsFragment fragment in group) {
if (fragment.config != null) {
final _SwitchableSemanticsFragment switchableFragment = fragment as _SwitchableSemanticsFragment;
switchableFragment._mergesToSibling = true;
node ??= fragment.owner._semantics;
if (configuration == null) {
switchableFragment._ensureConfigIsWritable();
configuration = switchableFragment.config;
} else {
configuration.absorb(switchableFragment.config!);
}
// It is a child fragment of a _SwitchableFragment, it must have a
// geometry.
final _SemanticsGeometry geometry = switchableFragment._computeSemanticsGeometry(
parentSemanticsClipRect: parentSemanticsClipRect,
parentPaintClipRect: parentPaintClipRect,
)!;
final Rect fragmentRect = MatrixUtils.transformRect(geometry.transform, geometry.rect);
if (rect == null) {
rect = fragmentRect;
} else {
rect = rect.expandToInclude(fragmentRect);
}
if (geometry.semanticsClipRect != null) {
final Rect rect = MatrixUtils.transformRect(geometry.transform, geometry.semanticsClipRect!);
if (semanticsClipRect == null) {
semanticsClipRect = rect;
} else {
semanticsClipRect = semanticsClipRect.intersect(rect);
}
}
if (geometry.paintClipRect != null) {
final Rect rect = MatrixUtils.transformRect(geometry.transform, geometry.paintClipRect!);
if (paintClipRect == null) {
paintClipRect = rect;
} else {
paintClipRect = paintClipRect.intersect(rect);
}
}
if (switchableFragment._tagsForChildren != null) {
tags.addAll(switchableFragment._tagsForChildren!);
}
}
}
// Can be null if all fragments in group are marked as explicit.
if (configuration != null && !rect!.isEmpty) {
if (node == null || usedSemanticsIds.contains(node.id)) {
node = SemanticsNode(showOnScreen: owner.showOnScreen);
}
usedSemanticsIds.add(node.id);
node
..tags = tags
..rect = rect
..transform = null // Will be set when compiling immediate parent node.
..parentSemanticsClipRect = semanticsClipRect
..parentPaintClipRect = paintClipRect;
for (final _InterestingSemanticsFragment fragment in group) {
if (fragment.config != null) {
fragment.owner._semantics = node;
}
}
node.updateWith(config: configuration);
result.add(node);
}
}
}
final List<_InterestingSemanticsFragment> _children = <_InterestingSemanticsFragment>[]; final List<_InterestingSemanticsFragment> _children = <_InterestingSemanticsFragment>[];
@override @override
void compileChildren({ Rect? parentSemanticsClipRect, Rect? parentPaintClipRect, required double elevationAdjustment, required List<SemanticsNode> result }) { void compileChildren({
Rect? parentSemanticsClipRect,
Rect? parentPaintClipRect,
required double elevationAdjustment,
required List<SemanticsNode> result,
required List<SemanticsNode> siblingNodes,
}) {
final Set<int> usedSemanticsIds = <int>{};
Iterable<_InterestingSemanticsFragment> compilingFragments = _children;
for (final List<_InterestingSemanticsFragment> siblingGroup in _siblingMergeGroups) {
compilingFragments = compilingFragments.followedBy(siblingGroup);
}
if (!_isExplicit) { if (!_isExplicit) {
if (!_mergesToSibling) {
owner._semantics = null; owner._semantics = null;
for (final _InterestingSemanticsFragment fragment in _children) { }
_mergeSiblingGroup(
parentSemanticsClipRect,
parentPaintClipRect,
siblingNodes,
usedSemanticsIds,
);
for (final _InterestingSemanticsFragment fragment in compilingFragments) {
assert(_ancestorChain.first == fragment._ancestorChain.last); assert(_ancestorChain.first == fragment._ancestorChain.last);
if (fragment is _SwitchableSemanticsFragment) {
// Cached semantics node may be part of sibling merging group prior
// to this update. In this case, the semantics node may continue to
// be reused in that sibling merging group.
if (fragment._isExplicit &&
fragment.owner._semantics != null &&
usedSemanticsIds.contains(fragment.owner._semantics!.id)) {
fragment.owner._semantics = null;
}
}
fragment._ancestorChain.addAll(_ancestorChain.skip(1)); fragment._ancestorChain.addAll(_ancestorChain.skip(1));
fragment.compileChildren( fragment.compileChildren(
parentSemanticsClipRect: parentSemanticsClipRect, parentSemanticsClipRect: parentSemanticsClipRect,
...@@ -4228,14 +4415,16 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment { ...@@ -4228,14 +4415,16 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
// its children are placed at the elevation dictated by this config. // its children are placed at the elevation dictated by this config.
elevationAdjustment: elevationAdjustment + _config.elevation, elevationAdjustment: elevationAdjustment + _config.elevation,
result: result, result: result,
siblingNodes: siblingNodes,
); );
} }
return; return;
} }
final _SemanticsGeometry? geometry = _needsGeometryUpdate final _SemanticsGeometry? geometry = _computeSemanticsGeometry(
? _SemanticsGeometry(parentSemanticsClipRect: parentSemanticsClipRect, parentPaintClipRect: parentPaintClipRect, ancestors: _ancestorChain) parentSemanticsClipRect: parentSemanticsClipRect,
: null; parentPaintClipRect: parentPaintClipRect,
);
if (!_mergeIntoParent && (geometry?.dropFromTree ?? false)) { if (!_mergeIntoParent && (geometry?.dropFromTree ?? false)) {
return; // Drop the node, it's not going to be visible. return; // Drop the node, it's not going to be visible.
...@@ -4264,22 +4453,66 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment { ...@@ -4264,22 +4453,66 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
_config.isHidden = true; _config.isHidden = true;
} }
} }
final List<SemanticsNode> children = <SemanticsNode>[]; final List<SemanticsNode> children = <SemanticsNode>[];
for (final _InterestingSemanticsFragment fragment in _children) { _mergeSiblingGroup(
node.parentSemanticsClipRect,
node.parentPaintClipRect,
siblingNodes,
usedSemanticsIds,
);
for (final _InterestingSemanticsFragment fragment in compilingFragments) {
if (fragment is _SwitchableSemanticsFragment) {
// Cached semantics node may be part of sibling merging group prior
// to this update. In this case, the semantics node may continue to
// be reused in that sibling merging group.
if (fragment._isExplicit &&
fragment.owner._semantics != null &&
usedSemanticsIds.contains(fragment.owner._semantics!.id)) {
fragment.owner._semantics = null;
}
}
final List<SemanticsNode> childSiblingNodes = <SemanticsNode>[];
fragment.compileChildren( fragment.compileChildren(
parentSemanticsClipRect: node.parentSemanticsClipRect, parentSemanticsClipRect: node.parentSemanticsClipRect,
parentPaintClipRect: node.parentPaintClipRect, parentPaintClipRect: node.parentPaintClipRect,
elevationAdjustment: 0.0, elevationAdjustment: 0.0,
result: children, result: children,
siblingNodes: childSiblingNodes,
); );
siblingNodes.addAll(childSiblingNodes);
} }
if (_config.isSemanticBoundary) { if (_config.isSemanticBoundary) {
owner.assembleSemanticsNode(node, _config, children); owner.assembleSemanticsNode(node, _config, children);
} else { } else {
node.updateWith(config: _config, childrenInInversePaintOrder: children); node.updateWith(config: _config, childrenInInversePaintOrder: children);
} }
result.add(node); result.add(node);
// Sibling node needs to attach to the parent of an explicit node.
for (final SemanticsNode siblingNode in siblingNodes) {
// sibling nodes are in the same coordinate of the immediate explicit node.
// They need to share the same transform if they are going to attach to the
// parent of the immediate explicit node.
assert(siblingNode.transform == null);
siblingNode
..transform = node.transform
..isMergedIntoParent = node.isMergedIntoParent;
if (_tagsForChildren != null) {
siblingNode.tags ??= <SemanticsTag>{};
siblingNode.tags!.addAll(_tagsForChildren!);
}
}
result.addAll(siblingNodes);
siblingNodes.clear();
}
_SemanticsGeometry? _computeSemanticsGeometry({
required Rect? parentSemanticsClipRect,
required Rect? parentPaintClipRect,
}) {
return _needsGeometryUpdate
? _SemanticsGeometry(parentSemanticsClipRect: parentSemanticsClipRect, parentPaintClipRect: parentPaintClipRect, ancestors: _ancestorChain)
: null;
} }
@override @override
......
...@@ -6,6 +6,7 @@ import 'dart:math' as math; ...@@ -6,6 +6,7 @@ import 'dart:math' as math;
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection; import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection;
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart' show MatrixUtils, TransformProperty; import 'package:flutter/painting.dart' show MatrixUtils, TransformProperty;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -53,6 +54,20 @@ typedef SemanticsActionHandler = void Function(Object? args); ...@@ -53,6 +54,20 @@ typedef SemanticsActionHandler = void Function(Object? args);
/// Used by [SemanticsOwner.onSemanticsUpdate]. /// Used by [SemanticsOwner.onSemanticsUpdate].
typedef SemanticsUpdateCallback = void Function(ui.SemanticsUpdate update); typedef SemanticsUpdateCallback = void Function(ui.SemanticsUpdate update);
/// Signature for the [SemanticsConfiguration.childConfigurationsDelegate].
///
/// The input list contains all [SemanticsConfiguration]s that rendering
/// children want to merge upward. One can tag a render child with a
/// [SemanticsTag] and look up its [SemanticsConfiguration]s through
/// [SemanticsConfiguration.tagsChildrenWith].
///
/// The return value is the arrangement of these configs, including which
/// configs continue to merge upward and which configs form sibling merge group.
///
/// Use [ChildSemanticsConfigurationsResultBuilder] to generate the return
/// value.
typedef ChildSemanticsConfigurationsDelegate = ChildSemanticsConfigurationsResult Function(List<SemanticsConfiguration>);
/// A tag for a [SemanticsNode]. /// A tag for a [SemanticsNode].
/// ///
/// Tags can be interpreted by the parent of a [SemanticsNode] /// Tags can be interpreted by the parent of a [SemanticsNode]
...@@ -85,6 +100,89 @@ class SemanticsTag { ...@@ -85,6 +100,89 @@ class SemanticsTag {
String toString() => '${objectRuntimeType(this, 'SemanticsTag')}($name)'; String toString() => '${objectRuntimeType(this, 'SemanticsTag')}($name)';
} }
/// The result that contains the arrangement for the child
/// [SemanticsConfiguration]s.
///
/// When the [PipelineOwner] builds the semantics tree, it uses the returned
/// [ChildSemanticsConfigurationsResult] from
/// [SemanticsConfiguration.childConfigurationsDelegate] to decide how semantics nodes
/// should form.
///
/// Use [ChildSemanticsConfigurationsResultBuilder] to build the result.
class ChildSemanticsConfigurationsResult {
ChildSemanticsConfigurationsResult._(this.mergeUp, this.siblingMergeGroups);
/// Returns the [SemanticsConfiguration]s that are supposed to be merged into
/// the parent semantics node.
///
/// [SemanticsConfiguration]s that are either semantics boundaries or are
/// conflicting with other [SemanticsConfiguration]s will form explicit
/// semantics nodes. All others will be merged into the parent.
final List<SemanticsConfiguration> mergeUp;
/// The groups of child semantics configurations that want to merge together
/// and form a sibling [SemanticsNode].
///
/// All the [SemanticsConfiguration]s in a given group that are either
/// semantics boundaries or are conflicting with other
/// [SemanticsConfiguration]s of the same group will be excluded from the
/// sibling merge group and form independent semantics nodes as usual.
///
/// The result [SemanticsNode]s from the merges are attached as the sibling
/// nodes of the immediate parent semantics node. For example, a `RenderObjectA`
/// has a rendering child, `RenderObjectB`. If both of them form their own
/// semantics nodes, `SemanticsNodeA` and `SemanticsNodeB`, any semantics node
/// created from sibling merge groups of `RenderObjectB` will be attach to
/// `SemanticsNodeA` as a sibling of `SemanticsNodeB`.
final List<List<SemanticsConfiguration>> siblingMergeGroups;
}
/// The builder to build a [ChildSemanticsConfigurationsResult] based on its
/// annotations.
///
/// To use this builder, one can use [markAsMergeUp] and
/// [markAsSiblingMergeGroup] to annotate the arrangement of
/// [SemanticsConfiguration]s. Once all the configs are annotated, use [build]
/// to generate the [ChildSemanticsConfigurationsResult].
class ChildSemanticsConfigurationsResultBuilder {
/// Creates a [ChildSemanticsConfigurationsResultBuilder].
ChildSemanticsConfigurationsResultBuilder();
final List<SemanticsConfiguration> _mergeUp = <SemanticsConfiguration>[];
final List<List<SemanticsConfiguration>> _siblingMergeGroups = <List<SemanticsConfiguration>>[];
/// Marks the [SemanticsConfiguration] to be merged into the parent semantics
/// node.
///
/// The [SemanticsConfiguration] will be added to the
/// [ChildSemanticsConfigurationsResult.mergeUp] that this builder builds.
void markAsMergeUp(SemanticsConfiguration config) => _mergeUp.add(config);
/// Marks a group of [SemanticsConfiguration]s to merge together
/// and form a sibling [SemanticsNode].
///
/// The group of [SemanticsConfiguration]s will be added to the
/// [ChildSemanticsConfigurationsResult.siblingMergeGroups] that this builder builds.
void markAsSiblingMergeGroup(List<SemanticsConfiguration> configs) => _siblingMergeGroups.add(configs);
/// Builds a [ChildSemanticsConfigurationsResult] contains the arrangement.
ChildSemanticsConfigurationsResult build() {
assert((){
final Set<SemanticsConfiguration> seenConfigs = <SemanticsConfiguration>{};
for (final SemanticsConfiguration config in <SemanticsConfiguration>[..._mergeUp, ..._siblingMergeGroups.flattened]) {
assert(
seenConfigs.add(config),
'Duplicated SemanticsConfigurations. This can happen if the same '
'SemanticsConfiguration was marked twice in markAsMergeUp and/or '
'markAsSiblingMergeGroup'
);
}
return true;
}());
return ChildSemanticsConfigurationsResult._(_mergeUp, _siblingMergeGroups);
}
}
/// An identifier of a custom semantics action. /// An identifier of a custom semantics action.
/// ///
/// Custom semantics actions can be provided to make complex user /// Custom semantics actions can be provided to make complex user
...@@ -3724,6 +3822,25 @@ class SemanticsConfiguration { ...@@ -3724,6 +3822,25 @@ class SemanticsConfiguration {
_onDidLoseAccessibilityFocus = value; _onDidLoseAccessibilityFocus = value;
} }
/// A delegate that decides how to handle [SemanticsConfiguration]s produced
/// in the widget subtree.
///
/// The [SemanticsConfiguration]s are produced by rendering objects in the
/// subtree and want to merge up to their parent. This delegate can decide
/// which of these should be merged together to form sibling SemanticsNodes and
/// which of them should be merged upwards into the parent SemanticsNode.
///
/// The input list of [SemanticsConfiguration]s can be empty if the rendering
/// object of this semantics configuration is a leaf node.
ChildSemanticsConfigurationsDelegate? get childConfigurationsDelegate => _childConfigurationsDelegate;
ChildSemanticsConfigurationsDelegate? _childConfigurationsDelegate;
set childConfigurationsDelegate(ChildSemanticsConfigurationsDelegate? value) {
assert(value != null);
_childConfigurationsDelegate = value;
// Setting the childConfigsDelegate does not annotate any meaningful
// semantics information of the config.
}
/// Returns the action handler registered for [action] or null if none was /// Returns the action handler registered for [action] or null if none was
/// registered. /// registered.
SemanticsActionHandler? getActionHandler(SemanticsAction action) => _actions[action]; SemanticsActionHandler? getActionHandler(SemanticsAction action) => _actions[action];
...@@ -4448,6 +4565,11 @@ class SemanticsConfiguration { ...@@ -4448,6 +4565,11 @@ class SemanticsConfiguration {
/// * [addTagForChildren] to add a tag and for more information about their /// * [addTagForChildren] to add a tag and for more information about their
/// usage. /// usage.
Iterable<SemanticsTag>? get tagsForChildren => _tagsForChildren; Iterable<SemanticsTag>? get tagsForChildren => _tagsForChildren;
/// Whether this configuration will tag the child semantics nodes with a
/// given [SemanticsTag].
bool tagsChildrenWith(SemanticsTag tag) => _tagsForChildren?.contains(tag) ?? false;
Set<SemanticsTag>? _tagsForChildren; Set<SemanticsTag>? _tagsForChildren;
/// Specifies a [SemanticsTag] that this configuration wants to apply to all /// Specifies a [SemanticsTag] that this configuration wants to apply to all
......
...@@ -4375,6 +4375,47 @@ void main() { ...@@ -4375,6 +4375,47 @@ void main() {
expect(prefixText.style, prefixStyle); expect(prefixText.style, prefixStyle);
}); });
testWidgets('TextField prefix and suffix create a sibling node', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
overlay(
child: TextField(
controller: TextEditingController(text: 'some text'),
decoration: const InputDecoration(
prefixText: 'Prefix',
suffixText: 'Suffix',
),
),
),
);
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
id: 2,
textDirection: TextDirection.ltr,
label: 'Prefix',
),
TestSemantics.rootChild(
id: 1,
textDirection: TextDirection.ltr,
value: 'some text',
actions: <SemanticsAction>[
SemanticsAction.tap,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
],
),
TestSemantics.rootChild(
id: 3,
textDirection: TextDirection.ltr,
label: 'Suffix',
),
],
), ignoreTransform: true, ignoreRect: true));
});
testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async { testWidgets('TextField with specified suffixStyle', (WidgetTester tester) async {
final TextStyle suffixStyle = TextStyle( final TextStyle suffixStyle = TextStyle(
color: Colors.pink[500], color: Colors.pink[500],
......
...@@ -1429,6 +1429,51 @@ void main() { ...@@ -1429,6 +1429,51 @@ void main() {
handle.dispose(); handle.dispose();
}); });
testWidgets('Two panel semantics is added to the sibling nodes of direct children', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
final UniqueKey key = UniqueKey();
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: ListView(
key: key,
children: const <Widget>[
TextField(
autofocus: true,
decoration: InputDecoration(
prefixText: 'prefix',
),
),
],
),
),
));
// Wait for focus.
await tester.pumpAndSettle();
final SemanticsNode scrollableNode = tester.getSemantics(find.byKey(key));
SemanticsNode? intermediateNode;
scrollableNode.visitChildren((SemanticsNode node) {
intermediateNode = node;
return true;
});
SemanticsNode? syntheticScrollableNode;
intermediateNode!.visitChildren((SemanticsNode node) {
syntheticScrollableNode = node;
return true;
});
expect(syntheticScrollableNode!.hasFlag(ui.SemanticsFlag.hasImplicitScrolling), isTrue);
int numberOfChild = 0;
syntheticScrollableNode!.visitChildren((SemanticsNode node) {
expect(node.isTagged(RenderViewport.useTwoPaneSemantics), isTrue);
numberOfChild += 1;
return true;
});
expect(numberOfChild, 2);
handle.dispose();
});
testWidgets('Scroll inertia cancel event', (WidgetTester tester) async { testWidgets('Scroll inertia cancel event', (WidgetTester tester) async {
await pumpTest(tester, null); await pumpTest(tester, null);
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0); await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'semantics_tester.dart';
void main() {
testWidgets('Semantics can merge sibling group', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const SemanticsTag first = SemanticsTag('1');
const SemanticsTag second = SemanticsTag('2');
const SemanticsTag third = SemanticsTag('3');
ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
expect(configs.length, 3);
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
final List<SemanticsConfiguration> sibling = <SemanticsConfiguration>[];
// Merge first and third
for (final SemanticsConfiguration config in configs) {
if (config.tagsChildrenWith(first) || config.tagsChildrenWith(third)) {
sibling.add(config);
} else {
builder.markAsMergeUp(config);
}
}
builder.markAsSiblingMergeGroup(sibling);
return builder.build();
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
label: 'parent',
child: TestConfigDelegate(
delegate: delegate,
child: Column(
children: <Widget>[
Semantics(
label: '1',
tagForChildren: first,
child: const SizedBox(width: 100, height: 100),
// this tests that empty nodes disappear
),
Semantics(
label: '2',
tagForChildren: second,
child: const SizedBox(width: 100, height: 100),
),
Semantics(
label: '3',
tagForChildren: third,
child: const SizedBox(width: 100, height: 100),
),
],
),
),
),
),
);
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'parent\n2',
),
TestSemantics.rootChild(
label: '1\n3',
),
],
), ignoreId: true, ignoreRect: true, ignoreTransform: true));
});
testWidgets('Semantics can drop semantics config', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const SemanticsTag first = SemanticsTag('1');
const SemanticsTag second = SemanticsTag('2');
const SemanticsTag third = SemanticsTag('3');
ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
// Merge first and third
for (final SemanticsConfiguration config in configs) {
if (config.tagsChildrenWith(first) || config.tagsChildrenWith(third)) {
continue;
}
builder.markAsMergeUp(config);
}
return builder.build();
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
label: 'parent',
child: TestConfigDelegate(
delegate: delegate,
child: Column(
children: <Widget>[
Semantics(
label: '1',
tagForChildren: first,
child: const SizedBox(width: 100, height: 100),
// this tests that empty nodes disappear
),
Semantics(
label: '2',
tagForChildren: second,
child: const SizedBox(width: 100, height: 100),
),
Semantics(
label: '3',
tagForChildren: third,
child: const SizedBox(width: 100, height: 100),
),
],
),
),
),
),
);
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'parent\n2',
),
],
), ignoreId: true, ignoreRect: true, ignoreTransform: true));
});
testWidgets('Semantics throws when mark the same config twice case 1', (WidgetTester tester) async {
const SemanticsTag first = SemanticsTag('1');
const SemanticsTag second = SemanticsTag('2');
const SemanticsTag third = SemanticsTag('3');
ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
// Marks the same one twice.
builder.markAsMergeUp(configs.first);
builder.markAsMergeUp(configs.first);
return builder.build();
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
label: 'parent',
child: TestConfigDelegate(
delegate: delegate,
child: Column(
children: <Widget>[
Semantics(
label: '1',
tagForChildren: first,
child: const SizedBox(width: 100, height: 100),
// this tests that empty nodes disappear
),
Semantics(
label: '2',
tagForChildren: second,
child: const SizedBox(width: 100, height: 100),
),
Semantics(
label: '3',
tagForChildren: third,
child: const SizedBox(width: 100, height: 100),
),
],
),
),
),
),
);
expect(tester.takeException(), isAssertionError);
});
testWidgets('Semantics throws when mark the same config twice case 2', (WidgetTester tester) async {
const SemanticsTag first = SemanticsTag('1');
const SemanticsTag second = SemanticsTag('2');
const SemanticsTag third = SemanticsTag('3');
ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
// Marks the same one twice.
builder.markAsMergeUp(configs.first);
builder.markAsSiblingMergeGroup(<SemanticsConfiguration>[configs.first]);
return builder.build();
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Semantics(
label: 'parent',
child: TestConfigDelegate(
delegate: delegate,
child: Column(
children: <Widget>[
Semantics(
label: '1',
tagForChildren: first,
child: const SizedBox(width: 100, height: 100),
// this tests that empty nodes disappear
),
Semantics(
label: '2',
tagForChildren: second,
child: const SizedBox(width: 100, height: 100),
),
Semantics(
label: '3',
tagForChildren: third,
child: const SizedBox(width: 100, height: 100),
),
],
),
),
),
),
);
expect(tester.takeException(), isAssertionError);
});
}
class TestConfigDelegate extends SingleChildRenderObjectWidget {
const TestConfigDelegate({super.key, required this.delegate, super.child});
final ChildSemanticsConfigurationsDelegate delegate;
@override
RenderTestConfigDelegate createRenderObject(BuildContext context) => RenderTestConfigDelegate(
delegate: delegate,
);
@override
void updateRenderObject(BuildContext context, RenderTestConfigDelegate renderObject) {
renderObject.delegate = delegate;
}
}
class RenderTestConfigDelegate extends RenderProxyBox {
RenderTestConfigDelegate({
ChildSemanticsConfigurationsDelegate? delegate,
}) : _delegate = delegate;
ChildSemanticsConfigurationsDelegate? get delegate => _delegate;
ChildSemanticsConfigurationsDelegate? _delegate;
set delegate(ChildSemanticsConfigurationsDelegate? value) {
if (value != _delegate) {
markNeedsSemanticsUpdate();
}
_delegate = value;
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
config.childConfigurationsDelegate = _delegate;
}
}
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