Skip to content

Commit 510a695

Browse files
adityasharatfacebook-github-bot
authored andcommitted
Spec-correct CSS Flexbox §4.5 auto-min-size opt-in on YGConfig (facebook#57015)
Summary: 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#57015 facebook/yoga#1966 Reviewed By: fbcbl Differential Revision: D105720159
1 parent 7cc8c76 commit 510a695

27 files changed

Lines changed: 697 additions & 4 deletions

packages/react-native/ReactAndroid/src/main/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?)

packages/react-native/ReactAndroid/src/main/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

packages/react-native/ReactAndroid/src/main/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")

packages/react-native/ReactAndroid/src/main/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

packages/react-native/ReactAndroid/src/main/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

packages/react-native/ReactAndroid/src/main/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)

packages/react-native/ReactAndroid/src/main/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 */

packages/react-native/ReactAndroid/src/main/jni/first-party/yogajni/jni/YGJNIVanilla.cpp

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,81 @@ 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+
static const jmethodID methodId = facebook::yoga::vanillajni::getMethodId(
698+
env, objectClass.get(), "measureMinContent", "(FIFI)J");
699+
const auto measureResult = facebook::yoga::vanillajni::callLongMethod(
700+
env, obj.get(), methodId, width, widthMode, height, heightMode);
701+
702+
uint32_t wBits = 0xFFFFFFFF & (measureResult >> 32);
703+
uint32_t hBits = 0xFFFFFFFF & measureResult;
704+
auto measuredWidth = std::bit_cast<float>(wBits);
705+
auto measuredHeight = std::bit_cast<float>(hBits);
706+
707+
return YGSize{measuredWidth, measuredHeight};
708+
} else {
709+
return YGSize{
710+
widthMode == YGMeasureModeUndefined ? 0 : width,
711+
heightMode == YGMeasureModeUndefined ? 0 : height,
712+
};
713+
}
714+
}
715+
716+
static void jni_YGNodeSetHasMinContentMeasureFuncJNI(
717+
JNIEnv* /*env*/,
718+
jobject /*obj*/,
719+
jlong nativePointer,
720+
jboolean hasMinContentMeasureFunc) {
721+
YGNodeSetMinContentMeasureFunc(
722+
_jlong2YGNodeRef(nativePointer),
723+
static_cast<bool>(hasMinContentMeasureFunc) ? YGJNIMinContentMeasureFunc
724+
: nullptr);
725+
}
726+
727+
static void jni_YGNodeSetMinContentWidthJNI(
728+
JNIEnv* /*env*/,
729+
jobject /*obj*/,
730+
jlong nativePointer,
731+
jfloat minContentWidth) {
732+
YGNodeSetMinContentWidth(
733+
_jlong2YGNodeRef(nativePointer), static_cast<float>(minContentWidth));
734+
}
735+
736+
static void jni_YGNodeSetMinContentHeightJNI(
737+
JNIEnv* /*env*/,
738+
jobject /*obj*/,
739+
jlong nativePointer,
740+
jfloat minContentHeight) {
741+
YGNodeSetMinContentHeight(
742+
_jlong2YGNodeRef(nativePointer), static_cast<float>(minContentHeight));
743+
}
744+
745+
static jfloat jni_YGNodeGetMinContentWidthJNI(
746+
JNIEnv* /*env*/,
747+
jobject /*obj*/,
748+
jlong nativePointer) {
749+
return static_cast<jfloat>(
750+
YGNodeGetMinContentWidth(_jlong2YGNodeRef(nativePointer)));
751+
}
752+
753+
static jfloat jni_YGNodeGetMinContentHeightJNI(
754+
JNIEnv* /*env*/,
755+
jobject /*obj*/,
756+
jlong nativePointer) {
757+
return static_cast<jfloat>(
758+
YGNodeGetMinContentHeight(_jlong2YGNodeRef(nativePointer)));
759+
}
760+
686761
static float YGJNIBaselineFunc(YGNodeConstRef node, float width, float height) {
687762
if (auto obj = YGNodeJobject(node)) {
688763
JNIEnv* env = getCurrentEnv();
@@ -1058,6 +1133,21 @@ static JNINativeMethod methods[] = {
10581133
{"jni_YGNodeSetHasMeasureFuncJNI",
10591134
"(JZ)V",
10601135
(void*)jni_YGNodeSetHasMeasureFuncJNI},
1136+
{"jni_YGNodeSetHasMinContentMeasureFuncJNI",
1137+
"(JZ)V",
1138+
(void*)jni_YGNodeSetHasMinContentMeasureFuncJNI},
1139+
{"jni_YGNodeSetMinContentWidthJNI",
1140+
"(JF)V",
1141+
(void*)jni_YGNodeSetMinContentWidthJNI},
1142+
{"jni_YGNodeSetMinContentHeightJNI",
1143+
"(JF)V",
1144+
(void*)jni_YGNodeSetMinContentHeightJNI},
1145+
{"jni_YGNodeGetMinContentWidthJNI",
1146+
"(J)F",
1147+
(void*)jni_YGNodeGetMinContentWidthJNI},
1148+
{"jni_YGNodeGetMinContentHeightJNI",
1149+
"(J)F",
1150+
(void*)jni_YGNodeGetMinContentHeightJNI},
10611151
{"jni_YGNodeStyleGetGapJNI", "(JI)J", (void*)jni_YGNodeStyleGetGapJNI},
10621152
{"jni_YGNodeStyleSetGapJNI", "(JIF)V", (void*)jni_YGNodeStyleSetGapJNI},
10631153
{"jni_YGNodeStyleSetGapPercentJNI",

packages/react-native/ReactCommon/yoga/yoga/YGEnums.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ const char* YGErrataToString(const YGErrata value) {
117117
return "absolute-position-without-insets-excludes-padding";
118118
case YGErrataAbsolutePercentAgainstInnerSize:
119119
return "absolute-percent-against-inner-size";
120+
case YGErrataMinSizeUndefinedInsteadOfAuto:
121+
return "min-size-undefined-instead-of-auto";
120122
case YGErrataAll:
121123
return "all";
122124
case YGErrataClassic:

packages/react-native/ReactCommon/yoga/yoga/YGEnums.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ YG_ENUM_DECL(
6767
YGErrataStretchFlexBasis = 1,
6868
YGErrataAbsolutePositionWithoutInsetsExcludesPadding = 2,
6969
YGErrataAbsolutePercentAgainstInnerSize = 4,
70+
YGErrataMinSizeUndefinedInsteadOfAuto = 8,
7071
YGErrataAll = 2147483647,
7172
YGErrataClassic = 2147483646)
7273
YG_DEFINE_ENUM_FLAG_OPERATORS(YGErrata)

0 commit comments

Comments
 (0)