Unverified Commit 7a3135b9 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Make `SemanticsNode.isMergedIntoParent` Readonly (#137304)

Fixes https://github.com/flutter/flutter/issues/54665
parent 3ef84471
......@@ -4880,7 +4880,6 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
}
final SemanticsNode node = (owner._semantics ??= SemanticsNode(showOnScreen: owner.showOnScreen))
..isMergedIntoParent = _mergeIntoParent
..tags = _tagsForChildren;
node.elevationAdjustment = elevationAdjustment;
......@@ -4942,9 +4941,7 @@ class _SwitchableSemanticsFragment extends _InterestingSemanticsFragment {
// 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;
siblingNode.transform = node.transform;
if (_tagsForChildren != null) {
siblingNode.tags ??= <SemanticsTag>{};
siblingNode.tags!.addAll(_tagsForChildren!);
......
......@@ -1230,7 +1230,7 @@ class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBo
final Rect paintRect = node.parentPaintClipRect!.intersect(currentRect);
configuration.isHidden = paintRect.isEmpty && !currentRect.isEmpty;
}
late final SemanticsNode newChild;
final SemanticsNode newChild;
if (_cachedChildNodes?.isNotEmpty ?? false) {
newChild = _cachedChildNodes!.remove(_cachedChildNodes!.keys.first)!;
} else {
......
......@@ -1811,15 +1811,11 @@ class SemanticsNode with DiagnosticableTreeMixin {
// MERGING
/// Whether this node merges its semantic information into an ancestor node.
bool get isMergedIntoParent => _isMergedIntoParent;
///
/// This value indicates whether this node has any ancestors with
/// [mergeAllDescendantsIntoThisNode] set to true.
bool get isMergedIntoParent => parent != null && _isMergedIntoParent;
bool _isMergedIntoParent = false;
set isMergedIntoParent(bool value) {
if (_isMergedIntoParent == value) {
return;
}
_isMergedIntoParent = value;
_markDirty();
}
/// Whether the user can interact with this node in assistive technologies.
///
......@@ -1865,46 +1861,6 @@ class SemanticsNode with DiagnosticableTreeMixin {
void _replaceChildren(List<SemanticsNode> newChildren) {
assert(!newChildren.any((SemanticsNode child) => child == this));
assert(() {
if (identical(newChildren, _children)) {
final List<DiagnosticsNode> mutationErrors = <DiagnosticsNode>[];
if (newChildren.length != _debugPreviousSnapshot.length) {
mutationErrors.add(ErrorDescription(
"The list's length has changed from ${_debugPreviousSnapshot.length} "
'to ${newChildren.length}.',
));
} else {
for (int i = 0; i < newChildren.length; i++) {
if (!identical(newChildren[i], _debugPreviousSnapshot[i])) {
if (mutationErrors.isNotEmpty) {
mutationErrors.add(ErrorSpacer());
}
mutationErrors.add(ErrorDescription('Child node at position $i was replaced:'));
mutationErrors.add(newChildren[i].toDiagnosticsNode(name: 'Previous child', style: DiagnosticsTreeStyle.singleLine));
mutationErrors.add(_debugPreviousSnapshot[i].toDiagnosticsNode(name: 'New child', style: DiagnosticsTreeStyle.singleLine));
}
}
}
if (mutationErrors.isNotEmpty) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.'),
ErrorHint('Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.'),
ErrorDescription('Error details:'),
...mutationErrors,
]);
}
}
assert(!newChildren.any((SemanticsNode node) => node.isMergedIntoParent) || isPartOfNodeMerging);
_debugPreviousSnapshot = List<SemanticsNode>.of(newChildren);
SemanticsNode ancestor = this;
while (ancestor.parent is SemanticsNode) {
ancestor = ancestor.parent!;
}
assert(!newChildren.any((SemanticsNode child) => child == ancestor));
return true;
}());
assert(() {
final Set<SemanticsNode> seenChildren = <SemanticsNode>{};
for (final SemanticsNode child in newChildren) {
......@@ -1920,7 +1876,6 @@ class SemanticsNode with DiagnosticableTreeMixin {
}
}
for (final SemanticsNode child in newChildren) {
assert(!child.isInvisible, 'Child $child is invisible and should not be added as a child of $this.');
child._dead = false;
}
bool sawChange = false;
......@@ -1951,6 +1906,47 @@ class SemanticsNode with DiagnosticableTreeMixin {
sawChange = true;
}
}
// Wait until the new children are adopted so isMergedIntoParent becomes
// up-to-date.
assert(() {
if (identical(newChildren, _children)) {
final List<DiagnosticsNode> mutationErrors = <DiagnosticsNode>[];
if (newChildren.length != _debugPreviousSnapshot.length) {
mutationErrors.add(ErrorDescription(
"The list's length has changed from ${_debugPreviousSnapshot.length} "
'to ${newChildren.length}.',
));
} else {
for (int i = 0; i < newChildren.length; i++) {
if (!identical(newChildren[i], _debugPreviousSnapshot[i])) {
if (mutationErrors.isNotEmpty) {
mutationErrors.add(ErrorSpacer());
}
mutationErrors.add(ErrorDescription('Child node at position $i was replaced:'));
mutationErrors.add(_debugPreviousSnapshot[i].toDiagnosticsNode(name: 'Previous child', style: DiagnosticsTreeStyle.singleLine));
mutationErrors.add(newChildren[i].toDiagnosticsNode(name: 'New child', style: DiagnosticsTreeStyle.singleLine));
}
}
}
if (mutationErrors.isNotEmpty) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.'),
ErrorHint('Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.'),
ErrorDescription('Error details:'),
...mutationErrors,
]);
}
}
_debugPreviousSnapshot = List<SemanticsNode>.of(newChildren);
SemanticsNode ancestor = this;
while (ancestor.parent is SemanticsNode) {
ancestor = ancestor.parent!;
}
assert(!newChildren.any((SemanticsNode child) => child == ancestor));
return true;
}());
if (!sawChange && _children != null) {
assert(newChildren.length == _children!.length);
// Did the order change?
......@@ -2045,6 +2041,28 @@ class SemanticsNode with DiagnosticableTreeMixin {
_children?.forEach(_redepthChild);
}
void _updateChildMergeFlagRecursively(SemanticsNode child) {
assert(child.owner == owner);
final bool childShouldMergeToParent = isPartOfNodeMerging;
if (childShouldMergeToParent == child._isMergedIntoParent) {
return;
}
child._isMergedIntoParent = childShouldMergeToParent;
_markDirty();
if (child.mergeAllDescendantsIntoThisNode) {
// No need to update the descendants since `child` has the merge flag set.
} else {
child._updateChildrenMergeFlags();
}
}
void _updateChildrenMergeFlags() {
_children?.forEach(_updateChildMergeFlagRecursively);
}
void _adoptChild(SemanticsNode child) {
assert(child._parent == null);
assert(() {
......@@ -2060,6 +2078,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
child.attach(_owner!);
}
_redepthChild(child);
_updateChildMergeFlagRecursively(child);
}
void _dropChild(SemanticsNode child) {
......@@ -2482,6 +2501,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
'SemanticsNodes with children must not specify a platformViewId.',
);
final bool mergeAllDescendantsIntoThisNodeValueChanged = _mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
_attributedLabel = config.attributedLabel;
_attributedValue = config.attributedValue;
_attributedIncreasedValue = config.attributedIncreasedValue;
......@@ -2512,6 +2533,10 @@ class SemanticsNode with DiagnosticableTreeMixin {
_areUserActionsBlocked = config.isBlockingUserActions;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
if (mergeAllDescendantsIntoThisNodeValueChanged) {
_updateChildrenMergeFlags();
}
assert(
!_canPerformAction(SemanticsAction.increase) || (value == '') == (increasedValue == ''),
'A SemanticsNode with action "increase" needs to be annotated with either both "value" and "increasedValue" or neither',
......@@ -3274,6 +3299,64 @@ class SemanticsOwner extends ChangeNotifier {
/// Update the semantics using [onSemanticsUpdate].
void sendSemanticsUpdate() {
// Once the tree is up-to-date, verify that every node is visible.
assert(() {
final List<SemanticsNode> invisibleNodes = <SemanticsNode>[];
// Finds the invisible nodes in the tree rooted at `node` and adds them to
// the invisibleNodes list. If a node is itself invisible, all its
// descendants will be skipped.
bool findInvisibleNodes(SemanticsNode node) {
if (node.rect.isEmpty) {
invisibleNodes.add(node);
} else if (!node.mergeAllDescendantsIntoThisNode) {
node.visitChildren(findInvisibleNodes);
}
return true;
}
final SemanticsNode? rootSemanticsNode = this.rootSemanticsNode;
if (rootSemanticsNode != null) {
// The root node is allowed to be invisible when it has no children.
if (rootSemanticsNode.childrenCount > 0 && rootSemanticsNode.rect.isEmpty) {
invisibleNodes.add(rootSemanticsNode);
} else if (!rootSemanticsNode.mergeAllDescendantsIntoThisNode) {
rootSemanticsNode.visitChildren(findInvisibleNodes);
}
}
if (invisibleNodes.isEmpty) {
return true;
}
List<DiagnosticsNode> nodeToMessage(SemanticsNode invisibleNode) {
final SemanticsNode? parent = invisibleNode.parent;
return<DiagnosticsNode>[
invisibleNode.toDiagnosticsNode(style: DiagnosticsTreeStyle.errorProperty),
parent?.toDiagnosticsNode(name: 'which was added as a child of', style: DiagnosticsTreeStyle.errorProperty) ?? ErrorDescription('which was added as the root SemanticsNode'),
];
}
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Invisible SemanticsNodes should not be added to the tree.'),
ErrorDescription('The following invisible SemanticsNodes were added to the tree:'),
...invisibleNodes.expand(nodeToMessage),
ErrorHint(
'An invisible SemanticsNode is one whose rect is not on screen hence not reachable for users, '
'and its semantic information is not merged into a visible parent.'
),
ErrorHint(
'An invisible SemantiscNode makes the accessibility experience confusing, '
'as it does not provide any visual indication when the user selects it '
'via accessibility technologies.'
),
ErrorHint(
'Consider removing the above invisible SemanticsNodes if they were added by your '
'RenderObject.assembleSemanticsNode implementation, or filing a bug on GitHub:\n'
' https://github.com/flutter/flutter/issues/new?template=2_bug.yml',
),
]);
}());
if (_dirtyNodes.isEmpty) {
return;
}
......
......@@ -1626,10 +1626,7 @@ class _RenderScrollSemantics extends RenderProxyBox {
return;
}
_innerNode ??= SemanticsNode(showOnScreen: showOnScreen);
_innerNode!
..isMergedIntoParent = node.isPartOfNodeMerging
..rect = node.rect;
(_innerNode ??= SemanticsNode(showOnScreen: showOnScreen)).rect = node.rect;
int? firstVisibleIndex;
final List<SemanticsNode> excluded = <SemanticsNode>[_innerNode!];
......
......@@ -58,7 +58,6 @@ void main() {
config: config,
childrenInInversePaintOrder: <SemanticsNode>[
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0)
..tags = tags,
],
......@@ -172,6 +171,135 @@ void main() {
);
});
test('provides the correct isMergedIntoParent value', () {
final SemanticsNode root = SemanticsNode()..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
final SemanticsNode node1 = SemanticsNode()..rect = const Rect.fromLTRB(1.0, 0.0, 10.0, 10.0);
final SemanticsNode node11 = SemanticsNode()..rect = const Rect.fromLTRB(2.0, 0.0, 10.0, 10.0);
final SemanticsNode node12 = SemanticsNode()..rect = const Rect.fromLTRB(3.0, 0.0, 10.0, 10.0);
final SemanticsConfiguration noMergeConfig = SemanticsConfiguration()
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = false;
final SemanticsConfiguration mergeConfig = SemanticsConfiguration()
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = true;
node1.updateWith(config: noMergeConfig, childrenInInversePaintOrder: <SemanticsNode>[node11, node12]);
expect(node1.isMergedIntoParent, false);
expect(node1.mergeAllDescendantsIntoThisNode, false);
expect(node11.isMergedIntoParent, false);
expect(node12.isMergedIntoParent, false);
expect(root.isMergedIntoParent, false);
root.updateWith(config: mergeConfig, childrenInInversePaintOrder: <SemanticsNode>[node1]);
expect(node1.isMergedIntoParent, true);
expect(node1.mergeAllDescendantsIntoThisNode, false);
expect(node11.isMergedIntoParent, true);
expect(node12.isMergedIntoParent, true);
expect(root.isMergedIntoParent, false);
expect(root.mergeAllDescendantsIntoThisNode, true);
// Change config
node1.updateWith(config: mergeConfig, childrenInInversePaintOrder: <SemanticsNode>[node11, node12]);
expect(node1.isMergedIntoParent, true);
expect(node1.mergeAllDescendantsIntoThisNode, true);
expect(node11.isMergedIntoParent, true);
expect(node12.isMergedIntoParent, true);
expect(root.isMergedIntoParent, false);
expect(root.mergeAllDescendantsIntoThisNode, true);
root.updateWith(config: noMergeConfig, childrenInInversePaintOrder: <SemanticsNode>[node1]);
expect(node1.isMergedIntoParent, false);
expect(node1.mergeAllDescendantsIntoThisNode, true);
expect(node11.isMergedIntoParent, true);
expect(node12.isMergedIntoParent, true);
expect(root.isMergedIntoParent, false);
expect(root.mergeAllDescendantsIntoThisNode, false);
});
test('sendSemanticsUpdate verifies no invisible nodes', () {
const Rect invisibleRect = Rect.fromLTRB(0.0, 0.0, 0.0, 10.0);
const Rect visibleRect = Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
final SemanticsOwner owner = SemanticsOwner(
onSemanticsUpdate: (SemanticsUpdate update) {},
);
final SemanticsNode root = SemanticsNode.root(owner: owner)..rect = invisibleRect;
final SemanticsNode child = SemanticsNode();
// It's ok to have an invisible root.
expect(owner.sendSemanticsUpdate, returnsNormally);
// It's ok to have an invisible child if it's merged to an ancestor.
root
..rect = visibleRect
..updateWith(
config: SemanticsConfiguration()
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = true,
childrenInInversePaintOrder: <SemanticsNode>[child..rect = invisibleRect],
);
expect(owner.sendSemanticsUpdate, returnsNormally);
// It's ok if all nodes are visible.
root
..rect = visibleRect
..updateWith(
config: SemanticsConfiguration()
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = false,
childrenInInversePaintOrder: <SemanticsNode>[child..rect = visibleRect],
);
expect(owner.sendSemanticsUpdate, returnsNormally);
// Invisible root with children bad.
root
..rect = invisibleRect
..updateWith(
config: SemanticsConfiguration()
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = true,
childrenInInversePaintOrder: <SemanticsNode>[child..rect = invisibleRect],
);
expect(owner.sendSemanticsUpdate, throwsA(isA<FlutterError>().having(
(FlutterError error) => error.message, 'message', equals(
'Invisible SemanticsNodes should not be added to the tree.\n'
'The following invisible SemanticsNodes were added to the tree:\n'
'SemanticsNode#0(dirty, merge boundary ⛔️, Rect.fromLTRB(0.0, 0.0, 0.0, 10.0), invisible)\n'
'which was added as the root SemanticsNode\n'
'An invisible SemanticsNode is one whose rect is not on screen hence not reachable for users, and its semantic information is not merged into a visible parent.\n'
'An invisible SemantiscNode makes the accessibility experience confusing, as it does not provide any visual indication when the user selects it via accessibility technologies.\n'
'Consider removing the above invisible SemanticsNodes if they were added by your RenderObject.assembleSemanticsNode implementation, or filing a bug on GitHub:\n'
' https://github.com/flutter/flutter/issues/new?template=2_bug.yml'
),
)));
// Invisible children bad.
root
..rect = visibleRect
..updateWith(
config: SemanticsConfiguration()
..isSemanticBoundary = true
..isMergingSemanticsOfDescendants = false,
childrenInInversePaintOrder: <SemanticsNode>[child..rect = invisibleRect],
);
expect(owner.sendSemanticsUpdate, throwsA(isA<FlutterError>().having(
(FlutterError error) => error.message, 'message', equals(
'Invisible SemanticsNodes should not be added to the tree.\n'
'The following invisible SemanticsNodes were added to the tree:\n'
'SemanticsNode#1(dirty, Rect.fromLTRB(0.0, 0.0, 0.0, 10.0), invisible)\n'
'which was added as a child of:\n'
' SemanticsNode#0(dirty, Rect.fromLTRB(0.0, 0.0, 10.0, 10.0))\n'
'An invisible SemanticsNode is one whose rect is not on screen hence not reachable for users, and its semantic information is not merged into a visible parent.\n'
'An invisible SemantiscNode makes the accessibility experience confusing, as it does not provide any visual indication when the user selects it via accessibility technologies.\n'
'Consider removing the above invisible SemanticsNodes if they were added by your RenderObject.assembleSemanticsNode implementation, or filing a bug on GitHub:\n'
' https://github.com/flutter/flutter/issues/new?template=2_bug.yml'
),
)));
});
test('mutate existing semantic node list errors', () {
final SemanticsNode node = SemanticsNode()
..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);
......@@ -182,7 +310,6 @@ void main() {
final List<SemanticsNode> children = <SemanticsNode>[
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0),
];
......@@ -193,8 +320,7 @@ void main() {
children.add(
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(42.0, 42.0, 10.0, 10.0),
..rect = const Rect.fromLTRB(42.0, 42.0, 52.0, 52.0),
);
{
......@@ -223,10 +349,8 @@ void main() {
late FlutterError error;
final List<SemanticsNode> modifiedChildren = <SemanticsNode>[
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0),
SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(10.0, 10.0, 20.0, 20.0),
];
node.updateWith(
......@@ -235,11 +359,9 @@ void main() {
);
try {
modifiedChildren[0] = SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(0.0, 0.0, 20.0, 20.0);
modifiedChildren[1] = SemanticsNode()
..isMergedIntoParent = true
..rect = const Rect.fromLTRB(40.0, 14.0, 20.0, 20.0);
..rect = const Rect.fromLTRB(40.0, 14.0, 60.0, 60.0);
node.updateWith(
config: config,
childrenInInversePaintOrder: modifiedChildren,
......@@ -255,12 +377,12 @@ void main() {
' containing the desired `SemanticsNode`s.\n'
' Error details:\n'
' Child node at position 0 was replaced:\n'
' Previous child: SemanticsNode#6(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(0.0, 0.0, 20.0, 20.0))\n'
' New child: SemanticsNode#4(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(5.0, 5.0, 10.0, 10.0))\n'
' Previous child: SemanticsNode#4(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(5.0, 5.0, 10.0, 10.0))\n'
' New child: SemanticsNode#6(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(0.0, 0.0, 20.0, 20.0))\n'
'\n'
' Child node at position 1 was replaced:\n'
' Previous child: SemanticsNode#7(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(40.0, 14.0, 20.0, 20.0))\n'
' New child: SemanticsNode#5(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(10.0, 10.0, 20.0, 20.0))\n',
' Previous child: SemanticsNode#5(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(10.0, 10.0, 20.0, 20.0))\n'
' New child: SemanticsNode#7(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(40.0, 14.0, 60.0, 60.0))\n',
));
expect(
......
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