Skip to content

Commit 171cd83

Browse files
sammy-SCfacebook-github-bot
authored andcommitted
Re-land: fixYogaFlexBasisFitContentInMainAxis
Summary: Re-land of D94658492 with fixes, which was reverted in D95669495. ## Context D94658492 added the `fixYogaFlexBasisFitContentInMainAxis` flag to avoid unnecessary re-measurement cascades in Yoga. When Yoga computes flex basis for container children, the legacy behavior applies a `FitContent` constraint in the main axis, bounding the child's measurement by the parent's available space. This creates a dependency between the child's flex basis and the parent's content-determined size — when one sibling changes size, all siblings get re-measured and their shadow nodes get cloned unnecessarily. The fix switches from `FitContent` to `MaxContent` for non-measure container children under auto-height parents, making each child's flex basis independent of the parent's size. ## What went wrong D94658492 modeled the fix as a `YogaErrata` bit (`FLEX_BASIS_FIT_CONTENT_IN_MAIN_AXIS`). Errata flags are bitmasks, and apps that opt into `ALL` or `CLASSIC` errata (like IGVR and Airwave) inadvertently picked up the new behavior without explicitly enabling the feature flag, causing breakages. ## What changed in this re-land This diff models the fix as a `YogaExperimentalFeature` (`FIX_FLEX_BASIS_FIT_CONTENT`) instead of a `YogaErrata` bit. Experimental features are individually opt-in, so existing apps won't accidentally pick up the change. This diff only wires up the RN feature flag infrastructure (flag defaults to `false`). The iOS MobileConfig override and the Yoga layout logic will be landed in follow-up diffs. changelog: [internal] Reviewed By: javache Differential Revision: D95852922
1 parent 2efbfb2 commit 171cd83

8 files changed

Lines changed: 334 additions & 13 deletions

File tree

enums.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env python3
1+
#!/usr/bin/env fbpython
22
# Copyright (c) Meta Platforms, Inc. and affiliates.
33
#
44
# This source code is licensed under the MIT license found in the
@@ -66,6 +66,9 @@
6666
"ExperimentalFeature": [
6767
# Mimic web flex-basis behavior (experiment may be broken)
6868
"WebFlexBasis",
69+
# Fix flex basis computation to not apply FitContent constraint in the
70+
# main axis for non-measure container nodes
71+
"FixFlexBasisFitContent",
6972
],
7073
"Gutter": ["Column", "Row", "All"],
7174
"GridTrackType": ["Auto", "Points", "Percent", "Fr", "Minmax"],

java/com/facebook/yoga/YogaExperimentalFeature.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
package com.facebook.yoga;
1111

1212
public enum YogaExperimentalFeature {
13-
WEB_FLEX_BASIS(0);
13+
WEB_FLEX_BASIS(0),
14+
FIX_FLEX_BASIS_FIT_CONTENT(1);
1415

1516
private final int mIntValue;
1617

@@ -25,6 +26,7 @@ public int intValue() {
2526
public static YogaExperimentalFeature fromInt(int value) {
2627
switch (value) {
2728
case 0: return WEB_FLEX_BASIS;
29+
case 1: return FIX_FLEX_BASIS_FIT_CONTENT;
2830
default: throw new IllegalArgumentException("Unknown enum value: " + value);
2931
}
3032
}

javascript/src/generated/YGEnums.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export enum Errata {
6767

6868
export enum ExperimentalFeature {
6969
WebFlexBasis = 0,
70+
FixFlexBasisFitContent = 1,
7071
}
7172

7273
export enum FlexDirection {
@@ -190,6 +191,7 @@ const constants = {
190191
ERRATA_ALL: Errata.All,
191192
ERRATA_CLASSIC: Errata.Classic,
192193
EXPERIMENTAL_FEATURE_WEB_FLEX_BASIS: ExperimentalFeature.WebFlexBasis,
194+
EXPERIMENTAL_FEATURE_FIX_FLEX_BASIS_FIT_CONTENT: ExperimentalFeature.FixFlexBasisFitContent,
193195
FLEX_DIRECTION_COLUMN: FlexDirection.Column,
194196
FLEX_DIRECTION_COLUMN_REVERSE: FlexDirection.ColumnReverse,
195197
FLEX_DIRECTION_ROW: FlexDirection.Row,
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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+
#include <algorithm>
11+
12+
static YGSize measureTextLike(
13+
YGNodeConstRef /*node*/,
14+
float width,
15+
YGMeasureMode widthMode,
16+
float /*height*/,
17+
YGMeasureMode /*heightMode*/) {
18+
float measuredWidth = 200.0f;
19+
if (widthMode == YGMeasureModeAtMost) {
20+
measuredWidth = std::min(measuredWidth, width);
21+
}
22+
return YGSize{.width = measuredWidth, .height = 20.0f};
23+
}
24+
25+
class YGFlexBasisFitContentTest : public testing::TestWithParam<bool> {
26+
protected:
27+
void SetUp() override {
28+
config_ = YGConfigNew();
29+
YGConfigSetExperimentalFeatureEnabled(
30+
config_, YGExperimentalFeatureFixFlexBasisFitContent, GetParam());
31+
}
32+
33+
void TearDown() override {
34+
if (root_ != nullptr) {
35+
YGNodeFreeRecursive(root_);
36+
}
37+
YGConfigFree(config_);
38+
}
39+
40+
YGConfigRef config_ = nullptr;
41+
YGNodeRef root_ = nullptr;
42+
};
43+
44+
// Auto-height container with a percentage-height child produces the same
45+
// layout regardless of feature state, because Check 3 preserves percentage
46+
// resolution when availableInnerHeight is NaN.
47+
TEST_P(YGFlexBasisFitContentTest, percentage_height_converges) {
48+
root_ = YGNodeNewWithConfig(config_);
49+
YGNodeStyleSetHeight(root_, 300);
50+
YGNodeStyleSetWidth(root_, 100);
51+
52+
YGNodeRef container = YGNodeNewWithConfig(config_);
53+
YGNodeInsertChild(root_, container, 0);
54+
55+
YGNodeRef child = YGNodeNewWithConfig(config_);
56+
YGNodeStyleSetHeightPercent(child, 50);
57+
YGNodeInsertChild(container, child, 0);
58+
59+
YGNodeCalculateLayout(root_, YGUndefined, YGUndefined, YGDirectionLTR);
60+
61+
ASSERT_FLOAT_EQ(75, YGNodeLayoutGetHeight(child));
62+
ASSERT_FLOAT_EQ(150, YGNodeLayoutGetHeight(container));
63+
}
64+
65+
// Two auto-height containers with percentage children and flexGrow:1 produce
66+
// the same layout regardless of feature state.
67+
TEST_P(YGFlexBasisFitContentTest, percentage_with_flex_grow_converges) {
68+
root_ = YGNodeNewWithConfig(config_);
69+
YGNodeStyleSetHeight(root_, 400);
70+
YGNodeStyleSetWidth(root_, 100);
71+
72+
YGNodeRef containerA = YGNodeNewWithConfig(config_);
73+
YGNodeStyleSetFlexGrow(containerA, 1);
74+
YGNodeInsertChild(root_, containerA, 0);
75+
76+
YGNodeRef childA = YGNodeNewWithConfig(config_);
77+
YGNodeStyleSetHeightPercent(childA, 25);
78+
YGNodeInsertChild(containerA, childA, 0);
79+
80+
YGNodeRef containerB = YGNodeNewWithConfig(config_);
81+
YGNodeStyleSetFlexGrow(containerB, 1);
82+
YGNodeInsertChild(root_, containerB, 1);
83+
84+
YGNodeRef childB = YGNodeNewWithConfig(config_);
85+
YGNodeStyleSetHeightPercent(childB, 50);
86+
YGNodeInsertChild(containerB, childB, 0);
87+
88+
YGNodeCalculateLayout(root_, YGUndefined, YGUndefined, YGDirectionLTR);
89+
90+
ASSERT_FLOAT_EQ(150, YGNodeLayoutGetHeight(containerA));
91+
ASSERT_FLOAT_EQ(250, YGNodeLayoutGetHeight(containerB));
92+
}
93+
94+
// Auto-height container with flexShrink and a percentage child causing
95+
// overflow produces the same layout regardless of feature state.
96+
TEST_P(YGFlexBasisFitContentTest, flex_shrink_overflow_converges) {
97+
root_ = YGNodeNewWithConfig(config_);
98+
YGNodeStyleSetHeight(root_, 200);
99+
YGNodeStyleSetWidth(root_, 100);
100+
101+
YGNodeRef container = YGNodeNewWithConfig(config_);
102+
YGNodeStyleSetFlexShrink(container, 1);
103+
YGNodeInsertChild(root_, container, 0);
104+
105+
YGNodeRef child = YGNodeNewWithConfig(config_);
106+
YGNodeStyleSetHeightPercent(child, 100);
107+
YGNodeInsertChild(container, child, 0);
108+
109+
YGNodeRef fixed = YGNodeNewWithConfig(config_);
110+
YGNodeStyleSetHeight(fixed, 150);
111+
YGNodeInsertChild(root_, fixed, 1);
112+
113+
YGNodeCalculateLayout(root_, YGUndefined, YGUndefined, YGDirectionLTR);
114+
115+
ASSERT_FLOAT_EQ(50, YGNodeLayoutGetHeight(container));
116+
ASSERT_FLOAT_EQ(150, YGNodeLayoutGetHeight(fixed));
117+
}
118+
119+
// In a scroll container (column), changing a sibling's height does not cause
120+
// re-measurement of unaffected subtrees when the feature is enabled.
121+
TEST_P(YGFlexBasisFitContentTest, scroll_avoids_remeasure) {
122+
static uint32_t measureCount = 0;
123+
auto measureFunc = [](YGNodeConstRef /*node*/,
124+
float /*width*/,
125+
YGMeasureMode /*widthMode*/,
126+
float /*height*/,
127+
YGMeasureMode /*heightMode*/) {
128+
measureCount++;
129+
return YGSize{.width = 50.0f, .height = 50.0f};
130+
};
131+
132+
measureCount = 0;
133+
134+
root_ = YGNodeNewWithConfig(config_);
135+
YGNodeStyleSetOverflow(root_, YGOverflowScroll);
136+
YGNodeStyleSetWidth(root_, 100);
137+
YGNodeStyleSetHeight(root_, 500);
138+
139+
YGNodeRef sibling = YGNodeNewWithConfig(config_);
140+
YGNodeStyleSetHeight(sibling, 100);
141+
YGNodeInsertChild(root_, sibling, 0);
142+
143+
YGNodeRef wrapper = YGNodeNewWithConfig(config_);
144+
YGNodeInsertChild(root_, wrapper, 1);
145+
146+
YGNodeRef inner = YGNodeNewWithConfig(config_);
147+
YGNodeInsertChild(wrapper, inner, 0);
148+
149+
YGNodeRef leaf = YGNodeNewWithConfig(config_);
150+
YGNodeSetMeasureFunc(leaf, measureFunc);
151+
YGNodeInsertChild(inner, leaf, 0);
152+
153+
YGNodeCalculateLayout(root_, YGUndefined, YGUndefined, YGDirectionLTR);
154+
uint32_t firstPassCount = measureCount;
155+
156+
YGNodeStyleSetHeight(sibling, 200);
157+
YGNodeCalculateLayout(root_, YGUndefined, YGUndefined, YGDirectionLTR);
158+
uint32_t secondPassCount = measureCount - firstPassCount;
159+
160+
ASSERT_FLOAT_EQ(50, YGNodeLayoutGetHeight(leaf));
161+
162+
EXPECT_EQ(0, secondPassCount);
163+
}
164+
165+
// Row direction is unaffected by the optimization. Width FitContent is always
166+
// preserved to support text wrapping through container nodes.
167+
TEST_P(YGFlexBasisFitContentTest, row_direction_unchanged) {
168+
root_ = YGNodeNewWithConfig(config_);
169+
YGNodeStyleSetWidth(root_, 100);
170+
YGNodeStyleSetHeight(root_, 100);
171+
172+
YGNodeRef container = YGNodeNewWithConfig(config_);
173+
YGNodeInsertChild(root_, container, 0);
174+
175+
YGNodeRef text = YGNodeNewWithConfig(config_);
176+
YGNodeSetMeasureFunc(text, measureTextLike);
177+
YGNodeInsertChild(container, text, 0);
178+
179+
YGNodeCalculateLayout(root_, YGUndefined, YGUndefined, YGDirectionLTR);
180+
181+
ASSERT_FLOAT_EQ(100, YGNodeLayoutGetWidth(text));
182+
}
183+
184+
// Scroll container in row direction: width FitContent is skipped for the
185+
// main axis (row) in scroll containers, matching legacy behavior.
186+
TEST_P(YGFlexBasisFitContentTest, row_scroll_skips_width) {
187+
root_ = YGNodeNewWithConfig(config_);
188+
YGNodeStyleSetFlexDirection(root_, YGFlexDirectionRow);
189+
YGNodeStyleSetOverflow(root_, YGOverflowScroll);
190+
YGNodeStyleSetWidth(root_, 100);
191+
YGNodeStyleSetHeight(root_, 100);
192+
193+
YGNodeRef text = YGNodeNewWithConfig(config_);
194+
YGNodeSetMeasureFunc(text, measureTextLike);
195+
YGNodeInsertChild(root_, text, 0);
196+
197+
YGNodeCalculateLayout(root_, YGUndefined, YGUndefined, YGDirectionLTR);
198+
199+
ASSERT_FLOAT_EQ(200, YGNodeLayoutGetWidth(text));
200+
}
201+
202+
INSTANTIATE_TEST_SUITE_P(
203+
YogaTest,
204+
YGFlexBasisFitContentTest,
205+
testing::Values(false, true));
206+
207+
// Feature toggle invalidates layout cache.
208+
TEST(YogaTest, flex_basis_fit_content_feature_change_invalidates_cache) {
209+
YGConfigRef config = YGConfigNew();
210+
YGConfigSetExperimentalFeatureEnabled(
211+
config, YGExperimentalFeatureFixFlexBasisFitContent, false);
212+
213+
YGNodeRef root = YGNodeNewWithConfig(config);
214+
YGNodeStyleSetHeight(root, 300);
215+
YGNodeStyleSetWidth(root, 100);
216+
217+
YGNodeRef container = YGNodeNewWithConfig(config);
218+
YGNodeStyleSetFlexGrow(container, 1);
219+
YGNodeInsertChild(root, container, 0);
220+
221+
YGNodeRef child = YGNodeNewWithConfig(config);
222+
YGNodeStyleSetHeightPercent(child, 50);
223+
YGNodeInsertChild(container, child, 0);
224+
225+
YGNodeRef fixed = YGNodeNewWithConfig(config);
226+
YGNodeStyleSetHeight(fixed, 100);
227+
YGNodeInsertChild(root, fixed, 1);
228+
229+
YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);
230+
float heightBefore = YGNodeLayoutGetHeight(container);
231+
232+
YGConfigSetExperimentalFeatureEnabled(
233+
config, YGExperimentalFeatureFixFlexBasisFitContent, true);
234+
YGNodeCalculateLayout(root, YGUndefined, YGUndefined, YGDirectionLTR);
235+
float heightAfter = YGNodeLayoutGetHeight(container);
236+
237+
ASSERT_FLOAT_EQ(heightBefore, heightAfter);
238+
239+
YGNodeFreeRecursive(root);
240+
YGConfigFree(config);
241+
}

yoga/YGEnums.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ const char* YGExperimentalFeatureToString(const YGExperimentalFeature value) {
129129
switch (value) {
130130
case YGExperimentalFeatureWebFlexBasis:
131131
return "web-flex-basis";
132+
case YGExperimentalFeatureFixFlexBasisFitContent:
133+
return "fix-flex-basis-fit-content";
132134
}
133135
return "unknown";
134136
}

yoga/YGEnums.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ YG_DEFINE_ENUM_FLAG_OPERATORS(YGErrata)
7373

7474
YG_ENUM_DECL(
7575
YGExperimentalFeature,
76-
YGExperimentalFeatureWebFlexBasis)
76+
YGExperimentalFeatureWebFlexBasis,
77+
YGExperimentalFeatureFixFlexBasisFitContent)
7778

7879
YG_ENUM_DECL(
7980
YGFlexDirection,

0 commit comments

Comments
 (0)