Skip to content

Commit 12baf5d

Browse files
authored
Merge branch 'main' into fix/flex-grow-zero-divisor-1665
2 parents b65866c + f6206ec commit 12baf5d

6 files changed

Lines changed: 240 additions & 9 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include <gtest/gtest.h>
9+
#include <yoga/Yoga.h>
10+
11+
namespace facebook::yoga {
12+
13+
// Regression test for `cleanupContentsNodesRecursively` stamping
14+
// `hasNewLayout=true` on a display:contents child during a measurement-only
15+
// visit.
16+
//
17+
// Setup: Root (overflow=visible, flex column) -> Parent (flex-grow=1)
18+
// -> Contents (display:contents) -> Leaf.
19+
// Flipping root's overflow between the two passes invalidates Parent's
20+
// measurement cache (computeFlexBasisForChild's `applyHeightFitContent`
21+
// branch flips) but leaves Parent's layout cache intact (its allotment is
22+
// unchanged). So in pass 2, Parent's calculateLayoutImpl runs only with
23+
// performLayout=false - the layout-phase visit is served from cache and
24+
// `cleanupContentsNodesRecursively` never runs at performLayout=true.
25+
TEST(YogaTest, contents_child_hasNewLayout_not_stamped_on_measure_only_visit) {
26+
YGNodeRef leaf = YGNodeNew();
27+
YGNodeStyleSetWidth(leaf, 20);
28+
YGNodeStyleSetHeight(leaf, 20);
29+
30+
YGNodeRef contents = YGNodeNew();
31+
YGNodeStyleSetDisplay(contents, YGDisplayContents);
32+
YGNodeInsertChild(contents, leaf, 0);
33+
34+
YGNodeRef parent = YGNodeNew();
35+
YGNodeStyleSetFlexGrow(parent, 1);
36+
YGNodeInsertChild(parent, contents, 0);
37+
38+
YGNodeRef root = YGNodeNew();
39+
YGNodeStyleSetFlexDirection(root, YGFlexDirectionColumn);
40+
YGNodeStyleSetWidth(root, 200);
41+
YGNodeStyleSetHeight(root, 200);
42+
YGNodeStyleSetOverflow(root, YGOverflowVisible);
43+
YGNodeInsertChild(root, parent, 0);
44+
45+
YGNodeCalculateLayout(root, 200, 200, YGDirectionLTR);
46+
47+
// Simulate a consumer (e.g. React Native's layout pass) reading and
48+
// clearing the hasNewLayout flags.
49+
YGNodeSetHasNewLayout(root, false);
50+
YGNodeSetHasNewLayout(parent, false);
51+
YGNodeSetHasNewLayout(contents, false);
52+
YGNodeSetHasNewLayout(leaf, false);
53+
54+
YGNodeStyleSetOverflow(root, YGOverflowScroll);
55+
YGNodeCalculateLayout(root, 200, 200, YGDirectionLTR);
56+
57+
EXPECT_FALSE(YGNodeGetHasNewLayout(contents))
58+
<< "contents.hasNewLayout was stamped during a measure-only visit "
59+
"(cleanupContentsNodesRecursively ran with performLayout=false but "
60+
"no matching performLayout=true visit occurred this pass)";
61+
62+
YGNodeFreeRecursive(root);
63+
}
64+
65+
// Regression test for `cleanupContentsNodesRecursively` invoked from
66+
// `layoutAbsoluteDescendants`: it must stamp `hasNewLayout=true` on
67+
// display:contents children on the path to an absolute descendant whose
68+
// position changed this pass. Otherwise consumers traversing the tree via
69+
// hasNewLayout would skip the contents subtree and miss the update.
70+
//
71+
// Setup: root (containing block) -> staticChild (fixed 50x50)
72+
// -> contents (display:contents) -> absoluteChild (right/bottom-
73+
// anchored so its position depends on the containing block).
74+
// Growing root in pass 2 dirties only root. staticChild's fixed dimensions
75+
// make its layout cache hit, so its main-path cleanup never runs.
76+
// absoluteChild depends on the containing block and is repositioned by
77+
// `layoutAbsoluteDescendants`, which is the only path that can stamp
78+
// contents along the way.
79+
TEST(
80+
YogaTest,
81+
absolute_descendant_through_contents_is_reachable_via_hasNewLayout) {
82+
YGNodeRef absoluteChild = YGNodeNew();
83+
YGNodeStyleSetPositionType(absoluteChild, YGPositionTypeAbsolute);
84+
YGNodeStyleSetPosition(absoluteChild, YGEdgeRight, 0);
85+
YGNodeStyleSetPosition(absoluteChild, YGEdgeBottom, 0);
86+
YGNodeStyleSetWidth(absoluteChild, 10);
87+
YGNodeStyleSetHeight(absoluteChild, 10);
88+
89+
YGNodeRef contents = YGNodeNew();
90+
YGNodeStyleSetDisplay(contents, YGDisplayContents);
91+
YGNodeInsertChild(contents, absoluteChild, 0);
92+
93+
YGNodeRef staticChild = YGNodeNew();
94+
YGNodeStyleSetPositionType(staticChild, YGPositionTypeStatic);
95+
YGNodeStyleSetWidth(staticChild, 50);
96+
YGNodeStyleSetHeight(staticChild, 50);
97+
YGNodeInsertChild(staticChild, contents, 0);
98+
99+
YGNodeRef root = YGNodeNew();
100+
YGNodeStyleSetWidth(root, 100);
101+
YGNodeStyleSetHeight(root, 100);
102+
YGNodeInsertChild(root, staticChild, 0);
103+
104+
YGNodeCalculateLayout(root, 100, 100, YGDirectionLTR);
105+
106+
// Simulate a consumer (e.g. React Native's layout pass) reading and
107+
// clearing the hasNewLayout flags.
108+
YGNodeSetHasNewLayout(root, false);
109+
YGNodeSetHasNewLayout(staticChild, false);
110+
YGNodeSetHasNewLayout(contents, false);
111+
YGNodeSetHasNewLayout(absoluteChild, false);
112+
113+
YGNodeStyleSetWidth(root, 150);
114+
YGNodeCalculateLayout(root, 150, 100, YGDirectionLTR);
115+
116+
ASSERT_TRUE(YGNodeGetHasNewLayout(absoluteChild));
117+
EXPECT_TRUE(YGNodeGetHasNewLayout(staticChild));
118+
EXPECT_TRUE(YGNodeGetHasNewLayout(contents))
119+
<< "contents node on the path to a freshly-positioned absolute "
120+
"descendant must have hasNewLayout=true so consumers can traverse "
121+
"to it";
122+
123+
YGNodeFreeRecursive(root);
124+
}
125+
126+
// Regression test for `cleanupContentsNodesRecursively` invoked from
127+
// `layoutAbsoluteDescendants`: it must not stamp `hasNewLayout=true` on
128+
// display:contents children when no new layout was produced for their
129+
// parent this pass. Otherwise the stale flag survives across passes and
130+
// can be observed by a later cache-hit on the parent.
131+
//
132+
// Setup: root -> a (fixed 50x50) -> b (fixed 30x30)
133+
// -> contents (display:contents) -> leaf.
134+
// Flipping root's overflow in pass 2 dirties only root. a and b have fixed
135+
// sizes so their layout caches hit; a.calculateLayoutImpl is skipped, so
136+
// b.calculateLayoutInternal is never invoked. `layoutAbsoluteDescendants`
137+
// still walks down through a and b looking for absolute descendants, but
138+
// there are none beneath b - so b.hasNewLayout stays false and the
139+
// cleanup along that walk must leave contents unflagged.
140+
TEST(
141+
YogaTest,
142+
absolute_phase_cleanup_does_not_stamp_when_parent_layout_skipped) {
143+
YGNodeRef leaf = YGNodeNew();
144+
YGNodeStyleSetWidth(leaf, 10);
145+
YGNodeStyleSetHeight(leaf, 10);
146+
147+
YGNodeRef contents = YGNodeNew();
148+
YGNodeStyleSetDisplay(contents, YGDisplayContents);
149+
YGNodeInsertChild(contents, leaf, 0);
150+
151+
YGNodeRef b = YGNodeNew();
152+
YGNodeStyleSetPositionType(b, YGPositionTypeStatic);
153+
YGNodeStyleSetWidth(b, 30);
154+
YGNodeStyleSetHeight(b, 30);
155+
YGNodeInsertChild(b, contents, 0);
156+
157+
YGNodeRef a = YGNodeNew();
158+
YGNodeStyleSetPositionType(a, YGPositionTypeStatic);
159+
YGNodeStyleSetWidth(a, 50);
160+
YGNodeStyleSetHeight(a, 50);
161+
YGNodeInsertChild(a, b, 0);
162+
163+
YGNodeRef root = YGNodeNew();
164+
YGNodeStyleSetWidth(root, 200);
165+
YGNodeStyleSetHeight(root, 200);
166+
YGNodeStyleSetOverflow(root, YGOverflowVisible);
167+
YGNodeInsertChild(root, a, 0);
168+
169+
YGNodeCalculateLayout(root, 200, 200, YGDirectionLTR);
170+
171+
// Simulate a consumer (e.g. React Native's layout pass) reading and
172+
// clearing the hasNewLayout flags.
173+
YGNodeSetHasNewLayout(root, false);
174+
YGNodeSetHasNewLayout(a, false);
175+
YGNodeSetHasNewLayout(b, false);
176+
YGNodeSetHasNewLayout(contents, false);
177+
YGNodeSetHasNewLayout(leaf, false);
178+
179+
YGNodeStyleSetOverflow(root, YGOverflowScroll);
180+
YGNodeCalculateLayout(root, 200, 200, YGDirectionLTR);
181+
182+
EXPECT_FALSE(YGNodeGetHasNewLayout(b));
183+
EXPECT_FALSE(YGNodeGetHasNewLayout(contents))
184+
<< "contents.hasNewLayout was stamped during a walk where its "
185+
"parent's hasNewLayout remained false this pass";
186+
187+
YGNodeFreeRecursive(root);
188+
}
189+
190+
} // namespace facebook::yoga

tests/YGDirtyMarkingTest.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,38 @@ TEST(YogaTest, dirty_removed_child_nodes_when_removing_all) {
304304
YGNodeFree(child1);
305305
YGNodeFreeRecursive(root);
306306
}
307+
308+
TEST(YogaTest, dirty_parent_when_child_freed) {
309+
YGNodeRef root = YGNodeNew();
310+
YGNodeStyleSetWidth(root, 100);
311+
YGNodeStyleSetHeight(root, 100);
312+
313+
YGNodeRef child = YGNodeNew();
314+
YGNodeStyleSetWidth(child, 50);
315+
YGNodeStyleSetHeight(child, 50);
316+
YGNodeInsertChild(root, child, 0);
317+
318+
YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);
319+
EXPECT_FALSE(YGNodeIsDirty(root));
320+
321+
YGNodeFree(child);
322+
323+
EXPECT_TRUE(YGNodeIsDirty(root));
324+
YGNodeFree(root);
325+
}
326+
327+
TEST(YogaTest, dirty_parent_when_subtree_freed_recursive) {
328+
YGNodeRef root = YGNodeNew();
329+
YGNodeRef child = YGNodeNew();
330+
YGNodeRef grandchild = YGNodeNew();
331+
YGNodeInsertChild(root, child, 0);
332+
YGNodeInsertChild(child, grandchild, 0);
333+
334+
YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);
335+
EXPECT_FALSE(YGNodeIsDirty(root));
336+
337+
YGNodeFreeRecursive(child);
338+
339+
EXPECT_TRUE(YGNodeIsDirty(root));
340+
YGNodeFree(root);
341+
}

yoga/YGNode.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ void YGNodeFree(const YGNodeRef nodeRef) {
4444
if (auto owner = node->getOwner()) {
4545
owner->removeChild(node);
4646
node->setOwner(nullptr);
47+
owner->markDirtyAndPropagate();
4748
}
4849

4950
const size_t childCount = node->getChildCount();

yoga/algorithm/AbsoluteLayout.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,6 @@ bool layoutAbsoluteDescendants(
558558
// we need to mutate these descendents. Make sure the path of
559559
// nodes to them is mutable before positioning.
560560
child->cloneChildrenIfNeeded();
561-
cleanupContentsNodesRecursively(child);
562561
const Direction childDirection =
563562
child->resolveDirection(currentNodeDirection);
564563
// By now all descendants of the containing block that are not absolute
@@ -584,6 +583,8 @@ bool layoutAbsoluteDescendants(
584583
containingNodeAvailableInnerHeight) ||
585584
hasNewLayout;
586585

586+
cleanupContentsNodesRecursively(
587+
child, /* didPerformLayout */ hasNewLayout);
587588
if (hasNewLayout) {
588589
child->setHasNewLayout(hasNewLayout);
589590
}

yoga/algorithm/CalculateLayout.cpp

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -516,19 +516,23 @@ void zeroOutLayoutRecursively(yoga::Node* const node) {
516516
}
517517
}
518518

519-
void cleanupContentsNodesRecursively(yoga::Node* const node) {
519+
void cleanupContentsNodesRecursively(
520+
yoga::Node* const node,
521+
bool didPerformLayout) {
520522
if (node->hasContentsChildren()) [[unlikely]] {
521523
node->cloneContentsChildrenIfNeeded();
522524
for (auto child : node->getChildren()) {
523525
if (child->style().display() == Display::Contents) {
524526
child->getLayout() = {};
525527
child->setLayoutDimension(0, Dimension::Width);
526528
child->setLayoutDimension(0, Dimension::Height);
527-
child->setHasNewLayout(true);
529+
if (didPerformLayout) {
530+
child->setHasNewLayout(true);
531+
}
528532
child->setDirty(false);
529533
child->cloneChildrenIfNeeded();
530534

531-
cleanupContentsNodesRecursively(child);
535+
cleanupContentsNodesRecursively(child, didPerformLayout);
532536
}
533537
}
534538
}
@@ -1624,7 +1628,7 @@ static void calculateLayoutImpl(
16241628

16251629
// Clean and update all display: contents nodes with a direct path to the
16261630
// current node as they will not be traversed
1627-
cleanupContentsNodesRecursively(node);
1631+
cleanupContentsNodesRecursively(node, performLayout);
16281632
return;
16291633
}
16301634

@@ -1642,7 +1646,7 @@ static void calculateLayoutImpl(
16421646

16431647
// Clean and update all display: contents nodes with a direct path to the
16441648
// current node as they will not be traversed
1645-
cleanupContentsNodesRecursively(node);
1649+
cleanupContentsNodesRecursively(node, performLayout);
16461650
return;
16471651
}
16481652

@@ -1660,7 +1664,7 @@ static void calculateLayoutImpl(
16601664
ownerHeight)) {
16611665
// Clean and update all display: contents nodes with a direct path to the
16621666
// current node as they will not be traversed
1663-
cleanupContentsNodesRecursively(node);
1667+
cleanupContentsNodesRecursively(node, /* didPerformLayout */ false);
16641668
return;
16651669
}
16661670

@@ -1672,7 +1676,7 @@ static void calculateLayoutImpl(
16721676

16731677
// Clean and update all display: contents nodes with a direct path to the
16741678
// current node as they will not be traversed
1675-
cleanupContentsNodesRecursively(node);
1679+
cleanupContentsNodesRecursively(node, performLayout);
16761680

16771681
// STEP 1: CALCULATE VALUES FOR REMAINDER OF ALGORITHM
16781682
const FlexDirection mainAxis =

yoga/algorithm/CalculateLayout.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@ float calculateAvailableInnerDimension(
5555

5656
void zeroOutLayoutRecursively(yoga::Node* const node);
5757

58-
void cleanupContentsNodesRecursively(yoga::Node* const node);
58+
void cleanupContentsNodesRecursively(yoga::Node* node, bool didPerformLayout);
5959

6060
} // namespace facebook::yoga

0 commit comments

Comments
 (0)