Skip to content

Commit aca45ae

Browse files
adityasharatfacebook-github-bot
authored andcommitted
Spec-correct CSS Flexbox §4.5 auto-min-size opt-in on YGConfig (#1966)
Summary: X-link: facebook/react-native#57015 Implements CSS Flexbox §4.5 automatic minimum sizing in Yoga. When opted in on a `YGConfig`, every flex item whose main-axis `min-{width,height}` is undefined receives a content-derived floor — `min(min-content, specified-size, max-size)` plus any aspect-ratio-transferred lower bound — so it cannot shrink below the size CSS browsers would honor. How to opt in: clear the new `YGErrataMinSizeUndefinedInsteadOfAuto` errata bit on the config. Default configs carry the bit (preserves today's behaviour); existing trees see no change. The bit is added to `YGErrataClassic` so consumers using that constant continue to get the same default. Three ways for a node to declare its min-content size along an axis, in this precedence order: 1. **Static per-axis value** via `YGNodeSetMinContentWidth/Height(node, value)`. Pass `YGUndefined` to clear. When set, the §4.5 probe short-circuits at this node — skipping both the dynamic callback and any container recursion. Use for known constants (e.g., images returning 0 per CSS-Images; scroll containers returning 0 on their scroll axis). 2. **Dynamic callback** via `YGNodeSetMinContentMeasureFunc(node, fn)`. Mirrors `YGMeasureFunc`'s shape. The algorithm invokes it for measure-func leaves when no static value is set. 3. **Default fallback**: the existing `YGMeasureFunc` invoked with `AtMost 0`, which text measurers naturally answer with their longest-word width. For container nodes without a static value, the algorithm walks children directly — sum on the container's own main axis, max on the cross axis — mirroring RenderCore FlexLayout's `computeMinContentSize`. This replaces the previous `calculateLayoutInternal(performLayout=false)` re-entry and skips cross-axis sizing, padding/border resolution, alignment, and baseline machinery that the probe doesn't need. Per-item opt-outs follow the CSS spec: explicit `min-{width,height}` (including 0), `display: none`, and `overflow != visible`. Cost when off (default): a single errata-bit check per flex line plus one `FloatOptional` short-circuit per `boundAxisWithAutoMin` call. Sub-microsecond. Cost when on: one min-content probe per opted-in flex item per layout pass. Static values short-circuit in ~30 ns; measure-func leaves cost one extra `measure()` call; container subtrees do one recursive sum/max walk over in-flow descendants. Reference: https://www.w3.org/TR/css-flexbox-1/#min-size-auto. Modeled after `xplat/flexlayout/`'s `MinSizeUndefinedInsteadOfAuto` errata path. Changelog: [General][Added] - Add CSS Flexbox §4.5 automatic minimum sizing. Opt in by clearing the new `YGErrataMinSizeUndefinedInsteadOfAuto` errata bit on `YGConfig`. Adds `YGNodeSetMinContentWidth/Height` for static contributions and `YGMinContentMeasureFunc` for dynamic ones.' GH pull request facebook/react-native#57015 #1966 Reviewed By: fbcbl Differential Revision: D105720159
1 parent c64d060 commit aca45ae

22 files changed

Lines changed: 1146 additions & 4 deletions

enums.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@
8686
# Absolute nodes will resolve percentages against the inner size of
8787
# their containing node, not the padding box
8888
("AbsolutePercentAgainstInnerSize", 1 << 2),
89+
# Treat main-axis `min-{width,height}: undefined` as "no floor"
90+
# instead of the CSS §4.5 automatic minimum (which derives a
91+
# content-based floor from the item's min-content size). Set by
92+
# default on new configs to preserve pre-§4.5 Yoga shrink behavior.
93+
# Clear this bit to opt into the spec-correct CSS §4.5 floor.
94+
("MinSizeUndefinedInsteadOfAuto", 1 << 3),
8995
# Enable all incorrect behavior (preserve compatibility)
9096
("All", 0x7FFFFFFF),
9197
# Enable all errata except for "StretchFlexBasis" (Defaults behavior

java/com/facebook/yoga/YogaConfig.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ public abstract class YogaConfig {
1919

2020
public abstract fun setErrata(errata: YogaErrata)
2121

22+
/**
23+
* Sets the errata bitmask directly from an [Int]. Use this when combining multiple [YogaErrata]
24+
* values (e.g., `YogaErrata.CLASSIC.intValue() and
25+
* YogaErrata.STRETCH_FLEX_BASIS.intValue().inv()`) — the [YogaErrata] enum cannot represent
26+
* arbitrary bitmask combinations.
27+
*/
28+
public abstract fun setErrata(errata: Int)
29+
2230
public abstract fun getErrata(): YogaErrata
2331

2432
public abstract fun setLogger(logger: YogaLogger?)

java/com/facebook/yoga/YogaConfigJNIBase.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ private constructor(@JvmField protected var nativePointer: Long) : YogaConfig()
5050
YogaNative.jni_YGConfigSetErrataJNI(nativePointer, errata.intValue())
5151
}
5252

53+
public override fun setErrata(errata: Int) {
54+
YogaNative.jni_YGConfigSetErrataJNI(nativePointer, errata)
55+
}
56+
5357
public override fun getErrata(): YogaErrata =
5458
YogaErrata.fromInt(YogaNative.jni_YGConfigGetErrataJNI(nativePointer))
5559

java/com/facebook/yoga/YogaErrata.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public enum class YogaErrata(public val intValue: Int) {
1414
STRETCH_FLEX_BASIS(1),
1515
ABSOLUTE_POSITION_WITHOUT_INSETS_EXCLUDES_PADDING(2),
1616
ABSOLUTE_PERCENT_AGAINST_INNER_SIZE(4),
17+
MIN_SIZE_UNDEFINED_INSTEAD_OF_AUTO(8),
1718
ALL(2147483647),
1819
CLASSIC(2147483646);
1920

@@ -27,6 +28,7 @@ public enum class YogaErrata(public val intValue: Int) {
2728
1 -> STRETCH_FLEX_BASIS
2829
2 -> ABSOLUTE_POSITION_WITHOUT_INSETS_EXCLUDES_PADDING
2930
4 -> ABSOLUTE_PERCENT_AGAINST_INNER_SIZE
31+
8 -> MIN_SIZE_UNDEFINED_INSTEAD_OF_AUTO
3032
2147483647 -> ALL
3133
2147483646 -> CLASSIC
3234
else -> throw IllegalArgumentException("Unknown enum value: $value")

java/com/facebook/yoga/YogaNative.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,22 @@ public object YogaNative {
311311
@JvmStatic
312312
public external fun jni_YGNodeSetHasMeasureFuncJNI(nativePointer: Long, hasMeasureFunc: Boolean)
313313

314+
@JvmStatic
315+
public external fun jni_YGNodeSetHasMinContentMeasureFuncJNI(
316+
nativePointer: Long,
317+
hasMinContentMeasureFunc: Boolean,
318+
)
319+
320+
@JvmStatic
321+
public external fun jni_YGNodeSetMinContentWidthJNI(nativePointer: Long, minContentWidth: Float)
322+
323+
@JvmStatic
324+
public external fun jni_YGNodeSetMinContentHeightJNI(nativePointer: Long, minContentHeight: Float)
325+
326+
@JvmStatic public external fun jni_YGNodeGetMinContentWidthJNI(nativePointer: Long): Float
327+
328+
@JvmStatic public external fun jni_YGNodeGetMinContentHeightJNI(nativePointer: Long): Float
329+
314330
@JvmStatic
315331
public external fun jni_YGNodeSetHasBaselineFuncJNI(nativePointer: Long, hasMeasureFunc: Boolean)
316332

java/com/facebook/yoga/YogaNode.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,16 @@ public abstract class YogaNode : YogaProps {
228228

229229
abstract override fun setMeasureFunction(measureFunction: YogaMeasureFunction?)
230230

231+
abstract override fun setMinContentMeasureFunction(measureFunction: YogaMeasureFunction?)
232+
233+
abstract override fun setMinContentWidth(minContentWidth: Float)
234+
235+
abstract override fun setMinContentHeight(minContentHeight: Float)
236+
237+
abstract override fun getMinContentWidth(): Float
238+
239+
abstract override fun getMinContentHeight(): Float
240+
231241
abstract override fun setBaselineFunction(yogaBaselineFunction: YogaBaselineFunction?)
232242

233243
public abstract val isMeasureDefined: Boolean

java/com/facebook/yoga/YogaNodeJNIBase.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public abstract class YogaNodeJNIBase : YogaNode, Cloneable {
1616
private var config: YogaConfig? = null
1717
private var children: MutableList<YogaNodeJNIBase>? = null
1818
private var measureFunction: YogaMeasureFunction? = null
19+
private var minContentMeasureFunction: YogaMeasureFunction? = null
1920
private var baselineFunction: YogaBaselineFunction? = null
2021
protected var nativePointer: Long = 0
2122

@@ -46,6 +47,7 @@ public abstract class YogaNodeJNIBase : YogaNode, Cloneable {
4647

4748
override fun reset() {
4849
measureFunction = null
50+
minContentMeasureFunction = null
4951
baselineFunction = null
5052
data = null
5153
arr = null
@@ -524,6 +526,25 @@ public abstract class YogaNodeJNIBase : YogaNode, Cloneable {
524526
YogaNative.jni_YGNodeSetHasMeasureFuncJNI(nativePointer, measureFunction != null)
525527
}
526528

529+
override fun setMinContentMeasureFunction(measureFunction: YogaMeasureFunction?) {
530+
this.minContentMeasureFunction = measureFunction
531+
YogaNative.jni_YGNodeSetHasMinContentMeasureFuncJNI(nativePointer, measureFunction != null)
532+
}
533+
534+
override fun setMinContentWidth(minContentWidth: Float) {
535+
YogaNative.jni_YGNodeSetMinContentWidthJNI(nativePointer, minContentWidth)
536+
}
537+
538+
override fun setMinContentHeight(minContentHeight: Float) {
539+
YogaNative.jni_YGNodeSetMinContentHeightJNI(nativePointer, minContentHeight)
540+
}
541+
542+
override fun getMinContentWidth(): Float =
543+
YogaNative.jni_YGNodeGetMinContentWidthJNI(nativePointer)
544+
545+
override fun getMinContentHeight(): Float =
546+
YogaNative.jni_YGNodeGetMinContentHeightJNI(nativePointer)
547+
527548
override fun setAlwaysFormsContainingBlock(alwaysFormsContainingBlock: Boolean) {
528549
YogaNative.jni_YGNodeSetAlwaysFormsContainingBlockJNI(
529550
nativePointer,
@@ -547,6 +568,27 @@ public abstract class YogaNodeJNIBase : YogaNode, Cloneable {
547568
)
548569
}
549570

571+
// Native callback invoked by Yoga during the CSS Flexbox §4.5 auto-min
572+
// probe when a min-content measure function is registered. Mirrors
573+
// [measure]; see that method's note on non-overridability.
574+
@DoNotStrip
575+
public fun measureMinContent(
576+
width: Float,
577+
widthMode: Int,
578+
height: Float,
579+
heightMode: Int,
580+
): Long {
581+
val mf =
582+
checkNotNull(minContentMeasureFunction) { "Min-content measure function isn't defined!" }
583+
return mf.measure(
584+
this,
585+
width,
586+
YogaMeasureMode.fromInt(widthMode),
587+
height,
588+
YogaMeasureMode.fromInt(heightMode),
589+
)
590+
}
591+
550592
override fun setBaselineFunction(yogaBaselineFunction: YogaBaselineFunction?) {
551593
baselineFunction = yogaBaselineFunction
552594
YogaNative.jni_YGNodeSetHasBaselineFuncJNI(nativePointer, yogaBaselineFunction != null)

java/com/facebook/yoga/YogaProps.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,28 @@ public interface YogaProps {
125125

126126
public fun setMeasureFunction(measureFunction: YogaMeasureFunction?)
127127

128+
public fun setMinContentMeasureFunction(measureFunction: YogaMeasureFunction?)
129+
130+
/**
131+
* Sets the static min-content width used by the CSS Flexbox §4.5 automatic minimum sizing probe.
132+
* Pass `YogaConstants.UNDEFINED` to clear. See `YGNodeSetMinContentWidth` in the Yoga C API for
133+
* full precedence rules.
134+
*/
135+
public fun setMinContentWidth(minContentWidth: Float)
136+
137+
/**
138+
* Sets the static min-content height used by the CSS Flexbox §4.5 automatic minimum sizing probe.
139+
* Pass `YogaConstants.UNDEFINED` to clear. See `YGNodeSetMinContentHeight` in the Yoga C API for
140+
* full precedence rules.
141+
*/
142+
public fun setMinContentHeight(minContentHeight: Float)
143+
144+
/** Returns the static min-content width, or `YogaConstants.UNDEFINED` if not set. */
145+
public fun getMinContentWidth(): Float
146+
147+
/** Returns the static min-content height, or `YogaConstants.UNDEFINED` if not set. */
148+
public fun getMinContentHeight(): Float
149+
128150
public fun setBaselineFunction(yogaBaselineFunction: YogaBaselineFunction?)
129151

130152
/* Mutable properties - getter and setter with matching types */

java/jni/YGJNIVanilla.cpp

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,83 @@ static void jni_YGNodeSetHasMeasureFuncJNI(
683683
static_cast<bool>(hasMeasureFunc) ? YGJNIMeasureFunc : nullptr);
684684
}
685685

686+
static YGSize YGJNIMinContentMeasureFunc(
687+
YGNodeConstRef node,
688+
float width,
689+
YGMeasureMode widthMode,
690+
float height,
691+
YGMeasureMode heightMode) {
692+
if (auto obj = YGNodeJobject(node)) {
693+
YGTransferLayoutDirection(node, obj.get());
694+
JNIEnv* env = getCurrentEnv();
695+
auto objectClass = facebook::yoga::vanillajni::make_local_ref(
696+
env, env->GetObjectClass(obj.get()));
697+
// NOLINTNEXTLINE(misc-misplaced-const)
698+
static const jmethodID methodId = facebook::yoga::vanillajni::getMethodId(
699+
env, objectClass.get(), "measureMinContent", "(FIFI)J");
700+
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
701+
const auto measureResult = facebook::yoga::vanillajni::callLongMethod(
702+
env, obj.get(), methodId, width, widthMode, height, heightMode);
703+
704+
uint32_t wBits = 0xFFFFFFFF & (measureResult >> 32);
705+
uint32_t hBits = 0xFFFFFFFF & measureResult;
706+
auto measuredWidth = std::bit_cast<float>(wBits);
707+
auto measuredHeight = std::bit_cast<float>(hBits);
708+
709+
return YGSize{measuredWidth, measuredHeight};
710+
} else {
711+
return YGSize{
712+
widthMode == YGMeasureModeUndefined ? 0 : width,
713+
heightMode == YGMeasureModeUndefined ? 0 : height,
714+
};
715+
}
716+
}
717+
718+
static void jni_YGNodeSetHasMinContentMeasureFuncJNI(
719+
JNIEnv* /*env*/,
720+
jobject /*obj*/,
721+
jlong nativePointer,
722+
jboolean hasMinContentMeasureFunc) {
723+
YGNodeSetMinContentMeasureFunc(
724+
_jlong2YGNodeRef(nativePointer),
725+
static_cast<bool>(hasMinContentMeasureFunc) ? YGJNIMinContentMeasureFunc
726+
: nullptr);
727+
}
728+
729+
static void jni_YGNodeSetMinContentWidthJNI(
730+
JNIEnv* /*env*/,
731+
jobject /*obj*/,
732+
jlong nativePointer,
733+
jfloat minContentWidth) {
734+
YGNodeSetMinContentWidth(
735+
_jlong2YGNodeRef(nativePointer), static_cast<float>(minContentWidth));
736+
}
737+
738+
static void jni_YGNodeSetMinContentHeightJNI(
739+
JNIEnv* /*env*/,
740+
jobject /*obj*/,
741+
jlong nativePointer,
742+
jfloat minContentHeight) {
743+
YGNodeSetMinContentHeight(
744+
_jlong2YGNodeRef(nativePointer), static_cast<float>(minContentHeight));
745+
}
746+
747+
static jfloat jni_YGNodeGetMinContentWidthJNI(
748+
JNIEnv* /*env*/,
749+
jobject /*obj*/,
750+
jlong nativePointer) {
751+
return static_cast<jfloat>(
752+
YGNodeGetMinContentWidth(_jlong2YGNodeRef(nativePointer)));
753+
}
754+
755+
static jfloat jni_YGNodeGetMinContentHeightJNI(
756+
JNIEnv* /*env*/,
757+
jobject /*obj*/,
758+
jlong nativePointer) {
759+
return static_cast<jfloat>(
760+
YGNodeGetMinContentHeight(_jlong2YGNodeRef(nativePointer)));
761+
}
762+
686763
static float YGJNIBaselineFunc(YGNodeConstRef node, float width, float height) {
687764
if (auto obj = YGNodeJobject(node)) {
688765
JNIEnv* env = getCurrentEnv();
@@ -1058,6 +1135,23 @@ static JNINativeMethod methods[] = {
10581135
{"jni_YGNodeSetHasMeasureFuncJNI",
10591136
"(JZ)V",
10601137
(void*)jni_YGNodeSetHasMeasureFuncJNI},
1138+
// NOLINTBEGIN(cppcoreguidelines-pro-type-cstyle-cast)
1139+
{"jni_YGNodeSetHasMinContentMeasureFuncJNI",
1140+
"(JZ)V",
1141+
(void*)jni_YGNodeSetHasMinContentMeasureFuncJNI},
1142+
{"jni_YGNodeSetMinContentWidthJNI",
1143+
"(JF)V",
1144+
(void*)jni_YGNodeSetMinContentWidthJNI},
1145+
{"jni_YGNodeSetMinContentHeightJNI",
1146+
"(JF)V",
1147+
(void*)jni_YGNodeSetMinContentHeightJNI},
1148+
{"jni_YGNodeGetMinContentWidthJNI",
1149+
"(J)F",
1150+
(void*)jni_YGNodeGetMinContentWidthJNI},
1151+
{"jni_YGNodeGetMinContentHeightJNI",
1152+
"(J)F",
1153+
(void*)jni_YGNodeGetMinContentHeightJNI},
1154+
// NOLINTEND(cppcoreguidelines-pro-type-cstyle-cast)
10611155
{"jni_YGNodeStyleGetGapJNI", "(JI)J", (void*)jni_YGNodeStyleGetGapJNI},
10621156
{"jni_YGNodeStyleSetGapJNI", "(JIF)V", (void*)jni_YGNodeStyleSetGapJNI},
10631157
{"jni_YGNodeStyleSetGapPercentJNI",

javascript/src/generated/YGEnums.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export enum Errata {
6161
StretchFlexBasis = 1,
6262
AbsolutePositionWithoutInsetsExcludesPadding = 2,
6363
AbsolutePercentAgainstInnerSize = 4,
64+
MinSizeUndefinedInsteadOfAuto = 8,
6465
All = 2147483647,
6566
Classic = 2147483646,
6667
}
@@ -188,6 +189,7 @@ const constants = {
188189
ERRATA_STRETCH_FLEX_BASIS: Errata.StretchFlexBasis,
189190
ERRATA_ABSOLUTE_POSITION_WITHOUT_INSETS_EXCLUDES_PADDING: Errata.AbsolutePositionWithoutInsetsExcludesPadding,
190191
ERRATA_ABSOLUTE_PERCENT_AGAINST_INNER_SIZE: Errata.AbsolutePercentAgainstInnerSize,
192+
ERRATA_MIN_SIZE_UNDEFINED_INSTEAD_OF_AUTO: Errata.MinSizeUndefinedInsteadOfAuto,
191193
ERRATA_ALL: Errata.All,
192194
ERRATA_CLASSIC: Errata.Classic,
193195
EXPERIMENTAL_FEATURE_WEB_FLEX_BASIS: ExperimentalFeature.WebFlexBasis,

0 commit comments

Comments
 (0)