Skip to content

Commit 7912e6d

Browse files
authored
Updates mark needs semantics update logic for overlay portal (#151688)
OverlayPortal attaches its overlaychild's renderobject to overlay directly while keeps its semantics tree under overlayportal. This become a problem when the `overlaychild` markNeedsSemanticsUpdate that it propagate upward the rendering tree. This means instead of marking dirty upward to the OverlayPortal, it directly mark Overlay dirty instead and skip `OverlayPortal`. Currently this does not pose an issue other than unnecessary rebuilds, but it become a problem when I try to optimize the semantics tree compilation flutter/flutter#150394. After the optimization it won't rebuild semantics node unless it is marked dirty. Since the OverlayPortal widget does not get marked dirty, it won't update the subtree.
1 parent 770c13b commit 7912e6d

File tree

3 files changed

+81
-6
lines changed

3 files changed

+81
-6
lines changed

packages/flutter/lib/src/rendering/object.dart

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1825,6 +1825,15 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
18251825
RenderObject? get parent => _parent;
18261826
RenderObject? _parent;
18271827

1828+
/// The semantics parent of this render object in the semantics tree.
1829+
///
1830+
/// This is typically the same as [parent].
1831+
///
1832+
/// [OverlayPortal] overrides this field to change how it forms its
1833+
/// semantics sub-tree.
1834+
@visibleForOverriding
1835+
RenderObject? get semanticsParent => _parent;
1836+
18281837
/// Called by subclasses when they decide a render object is a child.
18291838
///
18301839
/// Only for use by subclasses when changing their child lists. Calling this
@@ -3576,6 +3585,15 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
35763585
/// object, for accessibility purposes.
35773586
Rect get semanticBounds;
35783587

3588+
/// Whether the semantics of this render object is dirty and await the update.
3589+
///
3590+
/// Always returns false in release mode.
3591+
bool get debugNeedsSemanticsUpdate {
3592+
if (kReleaseMode) {
3593+
return false;
3594+
}
3595+
return _needsSemanticsUpdate;
3596+
}
35793597
bool _needsSemanticsUpdate = true;
35803598
SemanticsNode? _semantics;
35813599

@@ -3641,10 +3659,11 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
36413659
// node, thus marking this semantics boundary dirty is not enough, it needs
36423660
// to find the first parent semantics boundary that does not have any
36433661
// possible sibling node.
3644-
while (node.parent != null && (mayProduceSiblingNodes || !isEffectiveSemanticsBoundary)) {
3662+
while (node.semanticsParent != null && (mayProduceSiblingNodes || !isEffectiveSemanticsBoundary)) {
36453663
if (node != this && node._needsSemanticsUpdate) {
36463664
break;
36473665
}
3666+
36483667
node._needsSemanticsUpdate = true;
36493668
// Since this node is a semantics boundary, the produced sibling nodes will
36503669
// be attached to the parent semantics boundary. Thus, these sibling nodes
@@ -3653,7 +3672,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
36533672
mayProduceSiblingNodes = false;
36543673
}
36553674

3656-
node = node.parent!;
3675+
node = node.semanticsParent!;
36573676
isEffectiveSemanticsBoundary = node._semanticsConfiguration.isSemanticBoundary;
36583677
if (isEffectiveSemanticsBoundary && node._semantics == null) {
36593678
// We have reached a semantics boundary that doesn't own a semantics node.
@@ -3675,7 +3694,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
36753694
if (!node._needsSemanticsUpdate) {
36763695
node._needsSemanticsUpdate = true;
36773696
if (owner != null) {
3678-
assert(node._semanticsConfiguration.isSemanticBoundary || node.parent == null);
3697+
assert(node._semanticsConfiguration.isSemanticBoundary || node.semanticsParent == null);
36793698
owner!._nodesNeedingSemantics.add(node);
36803699
owner!.requestVisualUpdate();
36813700
}
@@ -3684,7 +3703,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
36843703

36853704
/// Updates the semantic information of the render object.
36863705
void _updateSemantics() {
3687-
assert(_semanticsConfiguration.isSemanticBoundary || parent == null);
3706+
assert(_semanticsConfiguration.isSemanticBoundary || semanticsParent == null);
36883707
if (_needsLayout) {
36893708
// There's not enough information in this subtree to compute semantics.
36903709
// The subtree is probably being kept alive by a viewport but not laid out.
@@ -3735,7 +3754,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
37353754
final bool blockChildInteractions = blockUserActions || config.isBlockingUserActions;
37363755
final bool childrenMergeIntoParent = mergeIntoParent || config.isMergingSemanticsOfDescendants;
37373756
final List<SemanticsConfiguration> childConfigurations = <SemanticsConfiguration>[];
3738-
final bool explicitChildNode = config.explicitChildNodes || parent == null;
3757+
final bool explicitChildNode = config.explicitChildNodes || semanticsParent == null;
37393758
final ChildSemanticsConfigurationsDelegate? childConfigurationsDelegate = config.childConfigurationsDelegate;
37403759
final Map<SemanticsConfiguration, _InterestingSemanticsFragment> configToFragment = <SemanticsConfiguration, _InterestingSemanticsFragment>{};
37413760
final List<_InterestingSemanticsFragment> mergeUpFragments = <_InterestingSemanticsFragment>[];
@@ -3816,7 +3835,7 @@ abstract class RenderObject with DiagnosticableTreeMixin implements HitTestTarge
38163835
_needsSemanticsUpdate = false;
38173836

38183837
final _SemanticsFragment result;
3819-
if (parent == null) {
3838+
if (semanticsParent == null) {
38203839
assert(!config.hasBeenAnnotated);
38213840
assert(!mergeIntoParent);
38223841
assert(siblingMergeFragmentGroups.isEmpty);

packages/flutter/lib/src/widgets/overlay.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2152,6 +2152,7 @@ class _OverlayPortalElement extends RenderObjectElement {
21522152
if (slot != null) {
21532153
renderObject._deferredLayoutChild = child as _RenderDeferredLayoutBox;
21542154
slot._addChild(child);
2155+
renderObject.markNeedsSemanticsUpdate();
21552156
} else {
21562157
renderObject.child = child;
21572158
}
@@ -2163,6 +2164,7 @@ class _OverlayPortalElement extends RenderObjectElement {
21632164
void moveRenderObjectChild(_RenderDeferredLayoutBox child, _OverlayEntryLocation oldSlot, _OverlayEntryLocation newSlot) {
21642165
assert(newSlot._debugIsLocationValid());
21652166
newSlot._moveChild(child, oldSlot);
2167+
renderObject.markNeedsSemanticsUpdate();
21662168
}
21672169

21682170
@override
@@ -2270,6 +2272,9 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterM
22702272
super.markNeedsLayout();
22712273
}
22722274

2275+
@override
2276+
RenderObject? get semanticsParent => _layoutSurrogate;
2277+
22732278
@override
22742279
double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) {
22752280
final RenderBox? child = this.child;

packages/flutter/test/widgets/overlay_portal_test.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,57 @@ void main() {
150150
expect(directionSeenByOverlayChild, textDirection);
151151
});
152152

153+
testWidgets('The overlay portal update semantics does not dirty overlay', (WidgetTester tester) async {
154+
late StateSetter setState;
155+
late final OverlayEntry overlayEntry;
156+
final UniqueKey overlayKey = UniqueKey();
157+
String msg = 'msg';
158+
addTearDown(() => overlayEntry..remove()..dispose());
159+
160+
await tester.pumpWidget(
161+
Directionality(
162+
textDirection: TextDirection.ltr,
163+
child: Semantics(
164+
container: true,
165+
child: Overlay(
166+
key: overlayKey,
167+
initialEntries: <OverlayEntry>[
168+
overlayEntry = OverlayEntry(
169+
builder: (BuildContext context) {
170+
return Semantics(
171+
container: true,
172+
explicitChildNodes: true,
173+
child: StatefulBuilder(
174+
builder: (BuildContext context, StateSetter setter) {
175+
setState = setter;
176+
return OverlayPortal(
177+
controller: controller1,
178+
overlayChildBuilder: (BuildContext context) {
179+
return Semantics(label: msg, child: const SizedBox(width: 100, height: 100));
180+
},
181+
child: const Text('overlay child'),
182+
);
183+
}
184+
),
185+
);
186+
},
187+
),
188+
],
189+
),
190+
),
191+
),
192+
);
193+
final RenderObject renderObject = tester.renderObject(find.byKey(overlayKey));
194+
expect(renderObject.debugNeedsSemanticsUpdate, isFalse);
195+
expect(find.bySemanticsLabel(msg), findsOneWidget);
196+
setState(() {
197+
msg = 'msg2';
198+
});
199+
// stop before updating semantics.
200+
await tester.pump(null, EnginePhase.composite);
201+
expect(renderObject.debugNeedsSemanticsUpdate, isFalse);
202+
});
203+
153204
testWidgets('Safe to deactivate and re-activate OverlayPortal', (WidgetTester tester) async {
154205
late final OverlayEntry overlayEntry;
155206
addTearDown(() => overlayEntry..remove()..dispose());

0 commit comments

Comments
 (0)