Unverified Commit 23a2fa31 authored by chunhtai's avatar chunhtai Committed by GitHub

Reland "Adds API in semanticsconfiguration to decide how to merge chi… (#116895)

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

This reverts commit 7549925c.

* makes markNeedsSemanticsUpdate more robust

* address comment
parent ab47fc30
......@@ -1329,6 +1329,35 @@ class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin
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;
......@@ -1716,12 +1745,16 @@ class _AffixText extends StatelessWidget {
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) {
......@@ -1731,7 +1764,11 @@ class _AffixText extends StatelessWidget {
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)),
),
),
);
}
......@@ -1903,6 +1940,11 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
late final Animation<double> _floatingLabelAnimation;
late final 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() {
......@@ -2227,22 +2269,42 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
),
);
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 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,
final bool hasPrefix = decoration.prefix != null || decoration.prefixText != null;
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,
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;
......@@ -2281,7 +2343,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
color: _getPrefixIconColor(themeData, defaults),
size: iconSize,
),
child: decoration.prefixIcon!,
child: Semantics(
child: decoration.prefixIcon,
),
),
),
),
......@@ -2306,7 +2370,9 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
color: _getSuffixIconColor(themeData, defaults),
size: iconSize,
),
child: decoration.suffixIcon!,
child: Semantics(
child: decoration.suffixIcon,
),
),
),
),
......@@ -2383,7 +2449,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
isDense: decoration.isDense,
visualDensity: themeData.visualDensity,
icon: icon,
input: widget.child,
input: input,
label: label,
hint: hint,
prefix: prefix,
......
......@@ -3100,6 +3100,10 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
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!;
}
......@@ -3160,15 +3164,30 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
// the semantics subtree starting at the identified semantics boundary.
final bool wasSemanticsBoundary = _semantics != null && (_cachedSemanticsConfiguration?.isSemanticBoundary ?? false);
bool mayProduceSiblingNodes =
_cachedSemanticsConfiguration?.childConfigurationsDelegate != null ||
_semanticsConfiguration.childConfigurationsDelegate != null;
_cachedSemanticsConfiguration = null;
bool isEffectiveSemanticsBoundary = _semanticsConfiguration.isSemanticBoundary && wasSemanticsBoundary;
RenderObject node = this;
while (!isEffectiveSemanticsBoundary && node.parent is RenderObject) {
// The sibling nodes will be attached to the parent of immediate semantics
// node, thus marking this semantics boundary dirty is not enough, it needs
// to find the first parent semantics boundary that does not have any
// possible sibling node.
while (node.parent is RenderObject && (mayProduceSiblingNodes || !isEffectiveSemanticsBoundary)) {
if (node != this && node._needsSemanticsUpdate) {
break;
}
node._needsSemanticsUpdate = true;
// Since this node is a semantics boundary, the produced sibling nodes will
// be attached to the parent semantics boundary. Thus, these sibling nodes
// will not be carried to the next loop.
if (isEffectiveSemanticsBoundary) {
mayProduceSiblingNodes = false;
}
node = node.parent! as RenderObject;
isEffectiveSemanticsBoundary = node._semanticsConfiguration.isSemanticBoundary;
......@@ -3213,15 +3232,16 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
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.
assert(interestingFragment.config == null && node == _semantics);
// Result may contain sibling nodes that are irrelevant for this update.
assert(interestingFragment.config == null && result.any((SemanticsNode node) => node == _semantics));
}
/// Returns the semantics that this node would like to add to its parent.
......@@ -3235,70 +3255,94 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
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 (!fragment.hasConfigForParent || producesForkingFragment) {
continue;
}
if (!config.isCompatibleWith(fragment.config)) {
toBeMarkedExplicit.add(fragment);
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);
}
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 +3351,34 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
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 +4051,9 @@ mixin RelayoutWhenSystemFontsChangeMixin on RenderObject {
/// * [_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 +4069,29 @@ abstract class _SemanticsFragment {
/// 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 +4128,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
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 +4158,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
bool get hasConfigForParent => config != null;
@override
List<_InterestingSemanticsFragment> get interestingFragments => <_InterestingSemanticsFragment>[this];
List<_InterestingSemanticsFragment> get mergeUpFragments => <_InterestingSemanticsFragment>[this];
Set<SemanticsTag>? _tagsForChildren;
......@@ -4124,7 +4196,13 @@ class _RootSemanticsFragment extends _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,
}) {
assert(_tagsForChildren == null || _tagsForChildren!.isEmpty);
assert(parentSemanticsClipRect == null);
assert(parentPaintClipRect == null);
......@@ -4150,8 +4228,11 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
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 +4282,11 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
_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 +4294,126 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
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 +4423,16 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
// 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 +4461,66 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
_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
......
......@@ -6,6 +6,7 @@ import 'dart:math' as math;
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 @@ typedef SemanticsActionHandler = void Function(Object? args);
/// 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 @@ class SemanticsTag {
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 @@ class SemanticsConfiguration {
_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 @@ class SemanticsConfiguration {
/// * [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
......
......@@ -4375,6 +4375,47 @@ void main() {
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],
......
......@@ -1429,6 +1429,51 @@ void main() {
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);
......
// 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);
});
testWidgets('RenderObject with semantics child delegate will mark correct boundary dirty', (WidgetTester tester) async {
final UniqueKey inner = UniqueKey();
final UniqueKey boundaryParent = UniqueKey();
final UniqueKey grandBoundaryParent = UniqueKey();
ChildSemanticsConfigurationsResult delegate(List<SemanticsConfiguration> configs) {
final ChildSemanticsConfigurationsResultBuilder builder = ChildSemanticsConfigurationsResultBuilder();
configs.forEach(builder.markAsMergeUp);
return builder.build();
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MarkSemanticsDirtySpy(
key: grandBoundaryParent,
child: MarkSemanticsDirtySpy(
key: boundaryParent,
child: TestConfigDelegate(
delegate: delegate,
child: Column(
children: <Widget>[
Semantics(
label: 'label',
child: MarkSemanticsDirtySpy(
key: inner,
child: const Text('inner'),
),
),
],
),
),
),
),
),
);
final RenderMarkSemanticsDirtySpy innerObject = tester.renderObject<RenderMarkSemanticsDirtySpy>(find.byKey(inner));
final RenderTestConfigDelegate objectWithDelegate = tester.renderObject<RenderTestConfigDelegate>(find.byType(TestConfigDelegate));
final RenderMarkSemanticsDirtySpy boundaryParentObject = tester.renderObject<RenderMarkSemanticsDirtySpy>(find.byKey(boundaryParent));
final RenderMarkSemanticsDirtySpy grandBoundaryParentObject = tester.renderObject<RenderMarkSemanticsDirtySpy>(find.byKey(grandBoundaryParent));
void resetBuildState() {
innerObject.hasRebuildSemantics = false;
boundaryParentObject.hasRebuildSemantics = false;
grandBoundaryParentObject.hasRebuildSemantics = false;
}
// Sanity check
expect(innerObject.hasRebuildSemantics, isTrue);
expect(boundaryParentObject.hasRebuildSemantics, isTrue);
expect(grandBoundaryParentObject.hasRebuildSemantics, isTrue);
resetBuildState();
innerObject.markNeedsSemanticsUpdate();
await tester.pump();
// Inner boundary should not trigger rebuild above it.
expect(innerObject.hasRebuildSemantics, isTrue);
expect(boundaryParentObject.hasRebuildSemantics, isFalse);
expect(grandBoundaryParentObject.hasRebuildSemantics, isFalse);
resetBuildState();
objectWithDelegate.markNeedsSemanticsUpdate();
await tester.pump();
// object with delegate rebuilds up to grand parent boundary;
expect(innerObject.hasRebuildSemantics, isTrue);
expect(boundaryParentObject.hasRebuildSemantics, isTrue);
expect(grandBoundaryParentObject.hasRebuildSemantics, isTrue);
resetBuildState();
boundaryParentObject.markNeedsSemanticsUpdate();
await tester.pump();
// Render objects in between child delegate and grand boundary parent does
// not mark the grand boundary parent dirty because it should not change the
// generated sibling nodes.
expect(innerObject.hasRebuildSemantics, isTrue);
expect(boundaryParentObject.hasRebuildSemantics, isTrue);
expect(grandBoundaryParentObject.hasRebuildSemantics, isFalse);
});
}
class MarkSemanticsDirtySpy extends SingleChildRenderObjectWidget {
const MarkSemanticsDirtySpy({super.key, super.child});
@override
RenderMarkSemanticsDirtySpy createRenderObject(BuildContext context) => RenderMarkSemanticsDirtySpy();
}
class RenderMarkSemanticsDirtySpy extends RenderProxyBox {
RenderMarkSemanticsDirtySpy();
bool hasRebuildSemantics = false;
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
config.isSemanticBoundary = true;
}
@override
void assembleSemanticsNode(
SemanticsNode node,
SemanticsConfiguration config,
Iterable<SemanticsNode> children,
) {
hasRebuildSemantics = true;
}
}
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