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
diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart
index 193c3b1..1f32086 100644
--- a/packages/flutter/lib/src/material/input_decorator.dart
+++ b/packages/flutter/lib/src/material/input_decorator.dart
@@ -1326,6 +1326,35 @@
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
void performLayout() {
final BoxConstraints constraints = this.constraints;
@@ -1713,12 +1742,16 @@
this.text,
this.style,
this.child,
+ this.semanticsSortKey,
+ required this.semanticsTag,
});
final bool labelIsFloating;
final String? text;
final TextStyle? style;
final Widget? child;
+ final SemanticsSortKey? semanticsSortKey;
+ final SemanticsTag semanticsTag;
@override
Widget build(BuildContext context) {
@@ -1728,7 +1761,11 @@
duration: _kTransitionDuration,
curve: _kTransitionCurve,
opacity: labelIsFloating ? 1.0 : 0.0,
- child: child ?? (text == null ? null : Text(text!, style: style)),
+ child: Semantics(
+ sortKey: semanticsSortKey,
+ tagForChildren: semanticsTag,
+ child: child ?? (text == null ? null : Text(text!, style: style)),
+ ),
),
);
}
@@ -1899,6 +1936,11 @@
late AnimationController _floatingLabelController;
late AnimationController _shakingLabelController;
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
void initState() {
@@ -2218,22 +2260,42 @@
),
);
- final Widget? prefix = decoration.prefix == null && decoration.prefixText == null ? null :
- _AffixText(
- labelIsFloating: widget._labelShouldWithdraw,
- text: decoration.prefixText,
- style: MaterialStateProperty.resolveAs(decoration.prefixStyle, materialState) ?? hintStyle,
- child: decoration.prefix,
- );
+ final bool hasPrefix = decoration.prefix != null || decoration.prefixText != null;
+ final bool hasSuffix = decoration.suffix != null || decoration.suffixText != null;
- final Widget? suffix = decoration.suffix == null && decoration.suffixText == null ? null :
- _AffixText(
- labelIsFloating: widget._labelShouldWithdraw,
- text: decoration.suffixText,
- style: MaterialStateProperty.resolveAs(decoration.suffixStyle, materialState) ?? hintStyle,
- child: decoration.suffix,
- );
+ 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,
+ text: decoration.prefixText,
+ style: MaterialStateProperty.resolveAs(decoration.prefixStyle, materialState) ?? hintStyle,
+ semanticsSortKey: needsSemanticsSortOrder ? _kPrefixSemanticsSortOrder : null,
+ semanticsTag: _kPrefixSemanticsTag,
+ child: decoration.prefix,
+ )
+ : null;
+
+ final Widget? suffix = hasSuffix
+ ? _AffixText(
+ labelIsFloating: widget._labelShouldWithdraw,
+ text: decoration.suffixText,
+ style: MaterialStateProperty.resolveAs(decoration.suffixStyle, materialState) ?? hintStyle,
+ semanticsSortKey: needsSemanticsSortOrder ? _kSuffixSemanticsSortOrder : null,
+ semanticsTag: _kSuffixSemanticsTag,
+ child: decoration.suffix,
+ )
+ : null;
+
+ if (input != null && needsSemanticsSortOrder) {
+ input = Semantics(
+ sortKey: _kInputSemanticsSortOrder,
+ child: input,
+ );
+ }
final bool decorationIsDense = decoration.isDense ?? false;
final double iconSize = decorationIsDense ? 18.0 : 24.0;
@@ -2272,7 +2334,9 @@
color: _getPrefixIconColor(themeData, defaults),
size: iconSize,
),
- child: decoration.prefixIcon!,
+ child: Semantics(
+ child: decoration.prefixIcon,
+ ),
),
),
),
@@ -2297,7 +2361,9 @@
color: _getSuffixIconColor(themeData, defaults),
size: iconSize,
),
- child: decoration.suffixIcon!,
+ child: Semantics(
+ child: decoration.suffixIcon,
+ ),
),
),
),
@@ -2374,7 +2440,7 @@
isDense: decoration.isDense,
visualDensity: themeData.visualDensity,
icon: icon,
- input: widget.child,
+ input: input,
label: label,
hint: hint,
prefix: prefix,
diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart
index c3aa820..7449016 100644
--- a/packages/flutter/lib/src/rendering/object.dart
+++ b/packages/flutter/lib/src/rendering/object.dart
@@ -3100,6 +3100,10 @@
if (_cachedSemanticsConfiguration == null) {
_cachedSemanticsConfiguration = SemanticsConfiguration();
describeSemanticsConfiguration(_cachedSemanticsConfiguration!);
+ assert(
+ !_cachedSemanticsConfiguration!.explicitChildNodes || _cachedSemanticsConfiguration!.childConfigurationsDelegate == null,
+ 'A SemanticsConfiguration with explicitChildNode set to true cannot have a non-null childConfigsDelegate.',
+ );
}
return _cachedSemanticsConfiguration!;
}
@@ -3161,7 +3165,13 @@
final bool wasSemanticsBoundary = _semantics != null && (_cachedSemanticsConfiguration?.isSemanticBoundary ?? false);
_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;
while (!isEffectiveSemanticsBoundary && node.parent is RenderObject) {
@@ -3213,11 +3223,13 @@
assert(fragment is _InterestingSemanticsFragment);
final _InterestingSemanticsFragment interestingFragment = fragment as _InterestingSemanticsFragment;
final List<SemanticsNode> result = <SemanticsNode>[];
+ final List<SemanticsNode> siblingNodes = <SemanticsNode>[];
interestingFragment.compileChildren(
parentSemanticsClipRect: _semantics?.parentSemanticsClipRect,
parentPaintClipRect: _semantics?.parentPaintClipRect,
elevationAdjustment: _semantics?.elevationAdjustment ?? 0.0,
result: result,
+ siblingNodes: siblingNodes,
);
final SemanticsNode node = result.single;
// Fragment only wants to add this node's SemanticsNode to the parent.
@@ -3235,70 +3247,94 @@
bool dropSemanticsOfPreviousSiblings = config.isBlockingSemanticsOfPreviouslyPaintedNodes;
final bool producesForkingFragment = !config.hasBeenAnnotated && !config.isSemanticBoundary;
- final List<_InterestingSemanticsFragment> fragments = <_InterestingSemanticsFragment>[];
- final Set<_InterestingSemanticsFragment> toBeMarkedExplicit = <_InterestingSemanticsFragment>{};
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) {
assert(!_needsLayout);
final _SemanticsFragment parentFragment = renderChild._getSemanticsForParent(
mergeIntoParent: childrenMergeIntoParent,
);
if (parentFragment.dropsSemanticsOfPreviousSiblings) {
- fragments.clear();
- toBeMarkedExplicit.clear();
+ childConfigurations.clear();
+ mergeUpFragments.clear();
+ siblingMergeFragmentGroups.clear();
if (!config.isSemanticBoundary) {
dropSemanticsOfPreviousSiblings = true;
}
}
- // Figure out which child fragments are to be made explicit.
- for (final _InterestingSemanticsFragment fragment in parentFragment.interestingFragments) {
- fragments.add(fragment);
+ for (final _InterestingSemanticsFragment fragment in parentFragment.mergeUpFragments) {
fragment.addAncestor(this);
fragment.addTags(config.tagsForChildren);
- if (config.explicitChildNodes || parent is! RenderObject) {
- fragment.markAsExplicit();
- continue;
+ if (hasChildConfigurationsDelegate && fragment.config != null) {
+ // This fragment need to go through delegate to determine whether it
+ // merge up or not.
+ childConfigurations.add(fragment.config!);
+ configToFragment[fragment.config!] = fragment;
+ } else {
+ mergeUpFragments.add(fragment);
}
- if (!fragment.hasConfigForParent || producesForkingFragment) {
- continue;
- }
- if (!config.isCompatibleWith(fragment.config)) {
- toBeMarkedExplicit.add(fragment);
- }
- final int siblingLength = fragments.length - 1;
- for (int i = 0; i < siblingLength; i += 1) {
- final _InterestingSemanticsFragment siblingFragment = fragments[i];
- if (!fragment.config!.isCompatibleWith(siblingFragment.config)) {
- toBeMarkedExplicit.add(fragment);
- toBeMarkedExplicit.add(siblingFragment);
+ }
+ if (parentFragment is _ContainerSemanticsFragment) {
+ // Container fragments needs to propagate sibling merge group to be
+ // compiled by _SwitchableSemanticsFragment.
+ for (final List<_InterestingSemanticsFragment> siblingMergeGroup in parentFragment.siblingMergeGroups) {
+ for (final _InterestingSemanticsFragment siblingMergingFragment in siblingMergeGroup) {
+ siblingMergingFragment.addAncestor(this);
+ siblingMergingFragment.addTags(config.tagsForChildren);
}
+ siblingMergeFragmentGroups.add(siblingMergeGroup);
}
}
});
- for (final _InterestingSemanticsFragment fragment in toBeMarkedExplicit) {
- fragment.markAsExplicit();
+ assert(hasChildConfigurationsDelegate || configToFragment.isEmpty);
+
+ if (explicitChildNode) {
+ for (final _InterestingSemanticsFragment fragment in mergeUpFragments) {
+ 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;
- _SemanticsFragment result;
+ final _SemanticsFragment result;
if (parent is! RenderObject) {
assert(!config.hasBeenAnnotated);
assert(!mergeIntoParent);
+ assert(siblingMergeFragmentGroups.isEmpty);
+ _marksExplicitInMergeGroup(mergeUpFragments, isMergeUp: true);
+ siblingMergeFragmentGroups.forEach(_marksExplicitInMergeGroup);
result = _RootSemanticsFragment(
owner: this,
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
);
} else if (producesForkingFragment) {
result = _ContainerSemanticsFragment(
+ siblingMergeGroups: siblingMergeFragmentGroups,
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
);
} else {
+ _marksExplicitInMergeGroup(mergeUpFragments, isMergeUp: true);
+ siblingMergeFragmentGroups.forEach(_marksExplicitInMergeGroup);
result = _SwitchableSemanticsFragment(
config: config,
mergeIntoParent: mergeIntoParent,
+ siblingMergeGroups: siblingMergeFragmentGroups,
owner: this,
dropsSemanticsOfPreviousSiblings: dropSemanticsOfPreviousSiblings,
);
@@ -3307,12 +3343,34 @@
fragment.markAsExplicit();
}
}
-
- result.addAll(fragments);
-
+ result.addAll(mergeUpFragments);
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.
///
/// The implementation has to return the children in paint order skipping all
@@ -3985,8 +4043,9 @@
/// * [_ContainerSemanticsFragment]: a container class to transport the semantic
/// information of multiple [_InterestingSemanticsFragment] to a parent.
abstract class _SemanticsFragment {
- _SemanticsFragment({ required this.dropsSemanticsOfPreviousSiblings })
- : assert (dropsSemanticsOfPreviousSiblings != null);
+ _SemanticsFragment({
+ required this.dropsSemanticsOfPreviousSiblings,
+ }) : assert (dropsSemanticsOfPreviousSiblings != null);
/// Incorporate the fragments of children into this fragment.
void addAll(Iterable<_InterestingSemanticsFragment> fragments);
@@ -4002,25 +4061,29 @@
/// Returns [_InterestingSemanticsFragment] describing the actual semantic
/// 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
/// [_InterestingSemanticsFragment] to its parent.
///
/// The [_InterestingSemanticsFragment] to be added to the parent can be
-/// obtained via [interestingFragments].
+/// obtained via [mergeUpFragments].
class _ContainerSemanticsFragment extends _SemanticsFragment {
+ _ContainerSemanticsFragment({
+ required super.dropsSemanticsOfPreviousSiblings,
+ required this.siblingMergeGroups,
+ });
- _ContainerSemanticsFragment({ required super.dropsSemanticsOfPreviousSiblings });
+ final List<List<_InterestingSemanticsFragment>> siblingMergeGroups;
@override
void addAll(Iterable<_InterestingSemanticsFragment> fragments) {
- interestingFragments.addAll(fragments);
+ mergeUpFragments.addAll(fragments);
}
@override
- final List<_InterestingSemanticsFragment> interestingFragments = <_InterestingSemanticsFragment>[];
+ final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
}
/// A [_SemanticsFragment] that describes which concrete semantic information
@@ -4057,6 +4120,7 @@
required Rect? parentPaintClipRect,
required double elevationAdjustment,
required List<SemanticsNode> result,
+ required List<SemanticsNode> siblingNodes,
});
/// The [SemanticsConfiguration] the child wants to merge into the parent's
@@ -4086,7 +4150,7 @@
bool get hasConfigForParent => config != null;
@override
- List<_InterestingSemanticsFragment> get interestingFragments => <_InterestingSemanticsFragment>[this];
+ List<_InterestingSemanticsFragment> get mergeUpFragments => <_InterestingSemanticsFragment>[this];
Set<SemanticsTag>? _tagsForChildren;
@@ -4124,7 +4188,13 @@
});
@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(parentSemanticsClipRect == null);
assert(parentPaintClipRect == null);
@@ -4150,8 +4220,11 @@
parentPaintClipRect: parentPaintClipRect,
elevationAdjustment: 0.0,
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);
// The root node is the only semantics node allowed to be invisible. This
@@ -4201,9 +4274,11 @@
_SwitchableSemanticsFragment({
required bool mergeIntoParent,
required SemanticsConfiguration config,
+ required List<List<_InterestingSemanticsFragment>> siblingMergeGroups,
required super.owner,
required super.dropsSemanticsOfPreviousSiblings,
- }) : _mergeIntoParent = mergeIntoParent,
+ }) : _siblingMergeGroups = siblingMergeGroups,
+ _mergeIntoParent = mergeIntoParent,
_config = config,
assert(mergeIntoParent != null),
assert(config != null);
@@ -4211,14 +4286,126 @@
final bool _mergeIntoParent;
SemanticsConfiguration _config;
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>[];
@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) {
- owner._semantics = null;
- for (final _InterestingSemanticsFragment fragment in _children) {
+ if (!_mergesToSibling) {
+ owner._semantics = null;
+ }
+ _mergeSiblingGroup(
+ parentSemanticsClipRect,
+ parentPaintClipRect,
+ siblingNodes,
+ usedSemanticsIds,
+ );
+ for (final _InterestingSemanticsFragment fragment in compilingFragments) {
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.compileChildren(
parentSemanticsClipRect: parentSemanticsClipRect,
@@ -4228,14 +4415,16 @@
// its children are placed at the elevation dictated by this config.
elevationAdjustment: elevationAdjustment + _config.elevation,
result: result,
+ siblingNodes: siblingNodes,
);
}
return;
}
- final _SemanticsGeometry? geometry = _needsGeometryUpdate
- ? _SemanticsGeometry(parentSemanticsClipRect: parentSemanticsClipRect, parentPaintClipRect: parentPaintClipRect, ancestors: _ancestorChain)
- : null;
+ final _SemanticsGeometry? geometry = _computeSemanticsGeometry(
+ parentSemanticsClipRect: parentSemanticsClipRect,
+ parentPaintClipRect: parentPaintClipRect,
+ );
if (!_mergeIntoParent && (geometry?.dropFromTree ?? false)) {
return; // Drop the node, it's not going to be visible.
@@ -4264,22 +4453,66 @@
_config.isHidden = true;
}
}
-
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(
parentSemanticsClipRect: node.parentSemanticsClipRect,
parentPaintClipRect: node.parentPaintClipRect,
elevationAdjustment: 0.0,
result: children,
+ siblingNodes: childSiblingNodes,
);
+ siblingNodes.addAll(childSiblingNodes);
}
+
if (_config.isSemanticBoundary) {
owner.assembleSemanticsNode(node, _config, children);
} else {
node.updateWith(config: _config, childrenInInversePaintOrder: children);
}
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
diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart
index bed9eea..041c70d 100644
--- a/packages/flutter/lib/src/semantics/semantics.dart
+++ b/packages/flutter/lib/src/semantics/semantics.dart
@@ -6,6 +6,7 @@
import 'dart:ui' as ui;
import 'dart:ui' show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection;
+import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart' show MatrixUtils, TransformProperty;
import 'package:flutter/services.dart';
@@ -53,6 +54,20 @@
/// Used by [SemanticsOwner.onSemanticsUpdate].
typedef SemanticsUpdateCallback = void Function(ui.SemanticsUpdate update);
+/// Signature for the [SemanticsConfiguration.childConfigurationsDelegate].
+///
+/// The input list contains all [SemanticsConfiguration]s that rendering
+/// children want to merge upward. One can tag a render child with a
+/// [SemanticsTag] and look up its [SemanticsConfiguration]s through
+/// [SemanticsConfiguration.tagsChildrenWith].
+///
+/// The return value is the arrangement of these configs, including which
+/// configs continue to merge upward and which configs form sibling merge group.
+///
+/// Use [ChildSemanticsConfigurationsResultBuilder] to generate the return
+/// value.
+typedef ChildSemanticsConfigurationsDelegate = ChildSemanticsConfigurationsResult Function(List<SemanticsConfiguration>);
+
/// A tag for a [SemanticsNode].
///
/// Tags can be interpreted by the parent of a [SemanticsNode]
@@ -85,6 +100,89 @@
String toString() => '${objectRuntimeType(this, 'SemanticsTag')}($name)';
}
+/// The result that contains the arrangement for the child
+/// [SemanticsConfiguration]s.
+///
+/// When the [PipelineOwner] builds the semantics tree, it uses the returned
+/// [ChildSemanticsConfigurationsResult] from
+/// [SemanticsConfiguration.childConfigurationsDelegate] to decide how semantics nodes
+/// should form.
+///
+/// Use [ChildSemanticsConfigurationsResultBuilder] to build the result.
+class ChildSemanticsConfigurationsResult {
+ ChildSemanticsConfigurationsResult._(this.mergeUp, this.siblingMergeGroups);
+
+ /// Returns the [SemanticsConfiguration]s that are supposed to be merged into
+ /// the parent semantics node.
+ ///
+ /// [SemanticsConfiguration]s that are either semantics boundaries or are
+ /// conflicting with other [SemanticsConfiguration]s will form explicit
+ /// semantics nodes. All others will be merged into the parent.
+ final List<SemanticsConfiguration> mergeUp;
+
+ /// The groups of child semantics configurations that want to merge together
+ /// and form a sibling [SemanticsNode].
+ ///
+ /// All the [SemanticsConfiguration]s in a given group that are either
+ /// semantics boundaries or are conflicting with other
+ /// [SemanticsConfiguration]s of the same group will be excluded from the
+ /// sibling merge group and form independent semantics nodes as usual.
+ ///
+ /// The result [SemanticsNode]s from the merges are attached as the sibling
+ /// nodes of the immediate parent semantics node. For example, a `RenderObjectA`
+ /// has a rendering child, `RenderObjectB`. If both of them form their own
+ /// semantics nodes, `SemanticsNodeA` and `SemanticsNodeB`, any semantics node
+ /// created from sibling merge groups of `RenderObjectB` will be attach to
+ /// `SemanticsNodeA` as a sibling of `SemanticsNodeB`.
+ final List<List<SemanticsConfiguration>> siblingMergeGroups;
+}
+
+/// The builder to build a [ChildSemanticsConfigurationsResult] based on its
+/// annotations.
+///
+/// To use this builder, one can use [markAsMergeUp] and
+/// [markAsSiblingMergeGroup] to annotate the arrangement of
+/// [SemanticsConfiguration]s. Once all the configs are annotated, use [build]
+/// to generate the [ChildSemanticsConfigurationsResult].
+class ChildSemanticsConfigurationsResultBuilder {
+ /// Creates a [ChildSemanticsConfigurationsResultBuilder].
+ ChildSemanticsConfigurationsResultBuilder();
+
+ final List<SemanticsConfiguration> _mergeUp = <SemanticsConfiguration>[];
+ final List<List<SemanticsConfiguration>> _siblingMergeGroups = <List<SemanticsConfiguration>>[];
+
+ /// Marks the [SemanticsConfiguration] to be merged into the parent semantics
+ /// node.
+ ///
+ /// The [SemanticsConfiguration] will be added to the
+ /// [ChildSemanticsConfigurationsResult.mergeUp] that this builder builds.
+ void markAsMergeUp(SemanticsConfiguration config) => _mergeUp.add(config);
+
+ /// Marks a group of [SemanticsConfiguration]s to merge together
+ /// and form a sibling [SemanticsNode].
+ ///
+ /// The group of [SemanticsConfiguration]s will be added to the
+ /// [ChildSemanticsConfigurationsResult.siblingMergeGroups] that this builder builds.
+ void markAsSiblingMergeGroup(List<SemanticsConfiguration> configs) => _siblingMergeGroups.add(configs);
+
+ /// Builds a [ChildSemanticsConfigurationsResult] contains the arrangement.
+ ChildSemanticsConfigurationsResult build() {
+ assert((){
+ final Set<SemanticsConfiguration> seenConfigs = <SemanticsConfiguration>{};
+ for (final SemanticsConfiguration config in <SemanticsConfiguration>[..._mergeUp, ..._siblingMergeGroups.flattened]) {
+ assert(
+ seenConfigs.add(config),
+ 'Duplicated SemanticsConfigurations. This can happen if the same '
+ 'SemanticsConfiguration was marked twice in markAsMergeUp and/or '
+ 'markAsSiblingMergeGroup'
+ );
+ }
+ return true;
+ }());
+ return ChildSemanticsConfigurationsResult._(_mergeUp, _siblingMergeGroups);
+ }
+}
+
/// An identifier of a custom semantics action.
///
/// Custom semantics actions can be provided to make complex user
@@ -3724,6 +3822,25 @@
_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
/// registered.
SemanticsActionHandler? getActionHandler(SemanticsAction action) => _actions[action];
@@ -4448,6 +4565,11 @@
/// * [addTagForChildren] to add a tag and for more information about their
/// usage.
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;
/// Specifies a [SemanticsTag] that this configuration wants to apply to all
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index 944e444..1cf7a24 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -4375,6 +4375,47 @@
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 {
final TextStyle suffixStyle = TextStyle(
color: Colors.pink[500],
diff --git a/packages/flutter/test/widgets/scrollable_test.dart b/packages/flutter/test/widgets/scrollable_test.dart
index 12b1839..4b5e379 100644
--- a/packages/flutter/test/widgets/scrollable_test.dart
+++ b/packages/flutter/test/widgets/scrollable_test.dart
@@ -1429,6 +1429,51 @@
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 {
await pumpTest(tester, null);
await tester.fling(find.byType(Scrollable), const Offset(0.0, -dragOffset), 1000.0);
diff --git a/packages/flutter/test/widgets/semantics_child_configs_delegate_test.dart b/packages/flutter/test/widgets/semantics_child_configs_delegate_test.dart
new file mode 100644
index 0000000..b09c4cd
--- /dev/null
+++ b/packages/flutter/test/widgets/semantics_child_configs_delegate_test.dart
@@ -0,0 +1,259 @@
+// 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;
+ }
+}