From f886f82c513953773389043b193e4a337f6d548d Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Tue, 20 Oct 2026 09:44:14 -0700 Subject: [PATCH] YogaNode.setNodeType selects a measured node's pixel rounding (#59235) Summary: Pull Request resolved: https://github.com/react/react-native/pull/59235 X-link: https://github.com/react/yoga/pull/2046 X-link: https://github.com/facebook/litho/pull/1093 **In one sentence.** A Java `YogaNode` with a measure function can now select Yoga's default pixel rounding, through a new `setNodeType` and its JNI binding to Yoga's existing `YGNodeSetNodeType`. ## Why this is needed Installing a measure function makes a Yoga node a text node, which is rounded to the pixel grid as text: its fractional size is rounded up, and its position down, so its content is not truncated. A container measured through a callback should round as a container does, each edge to the nearest pixel. The C API can select that (`YGNodeSetNodeType`); the Java binding could not. - Measured: of three nodes flexed to a third of a 100 wide row each, the first, given a measure function, ends at 33.33 and comes out 34 wide; without one, 33. ## What this diff does **`YogaNode.setNodeType(nodeType)`,** a new public open method that does nothing by default, so a `YogaNode` subclass built against an earlier class does not fail with `AbstractMethodError`. `YogaNodeJNIBase` overrides it through a new native method, `YogaNative.jni_YGNodeSetNodeTypeJNI`, which `YGJNIVanilla.cpp` registers and implements with `YGNodeSetNodeType`. `setMeasureFunction` still sets the type (text for a function, default for null), so a caller selects the type after it. ## What a Yoga user sees **A new public Yoga API.** **The Java and native halves must ship together.** `registerNatives` is strict: a Java class declaring `jni_YGNodeSetNodeTypeJNI` with a native library that lacks it, or the reverse, fails when the class registers. Changelog: [Android][Added] - Add `YogaNode.setNodeType` to select pixel rounding for measured nodes Reviewed By: christophpurrer Differential Revision: D125292521 --- .../src/main/java/com/facebook/yoga/YogaNative.kt | 2 ++ .../src/main/java/com/facebook/yoga/YogaNode.kt | 11 +++++++++++ .../main/java/com/facebook/yoga/YogaNodeJNIBase.kt | 4 ++++ .../main/jni/first-party/yogajni/jni/YGJNIVanilla.cpp | 10 ++++++++++ 4 files changed, 27 insertions(+) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNative.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNative.kt index c00957b14db0..9f09f26b09f9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNative.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNative.kt @@ -311,6 +311,8 @@ public object YogaNative { @JvmStatic public external fun jni_YGNodeSetHasMeasureFuncJNI(nativePointer: Long, hasMeasureFunc: Boolean) + @JvmStatic public external fun jni_YGNodeSetNodeTypeJNI(nativePointer: Long, nodeType: Int) + @JvmStatic public external fun jni_YGNodeSetHasMinContentMeasureFuncJNI( nativePointer: Long, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNode.kt index 08b84232c855..754ed7a27913 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNode.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNode.kt @@ -228,6 +228,17 @@ public abstract class YogaNode : YogaProps { abstract override fun setMeasureFunction(measureFunction: YogaMeasureFunction?) + /** + * Sets the node type, which selects how layout is rounded to the pixel grid: a + * [YogaNodeType.TEXT] node's fractional size is rounded up and its position down, so its content + * is not truncated. [setMeasureFunction] sets it ([YogaNodeType.TEXT] for a function, + * [YogaNodeType.DEFAULT] for null), so call this after it. + * + * Does nothing by default, so a subclass built against an earlier [YogaNode] keeps working, with + * the rounding [setMeasureFunction] selected. + */ + public open fun setNodeType(nodeType: YogaNodeType) {} + abstract override fun setMinContentMeasureFunction(measureFunction: YogaMeasureFunction?) abstract override fun setMinContentWidth(minContentWidth: Float) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNodeJNIBase.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNodeJNIBase.kt index 484865c163db..9e2b1fe14e1b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNodeJNIBase.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/yoga/YogaNodeJNIBase.kt @@ -526,6 +526,10 @@ public abstract class YogaNodeJNIBase : YogaNode, Cloneable { YogaNative.jni_YGNodeSetHasMeasureFuncJNI(nativePointer, measureFunction != null) } + override fun setNodeType(nodeType: YogaNodeType) { + YogaNative.jni_YGNodeSetNodeTypeJNI(nativePointer, nodeType.intValue()) + } + override fun setMinContentMeasureFunction(measureFunction: YogaMeasureFunction?) { this.minContentMeasureFunction = measureFunction YogaNative.jni_YGNodeSetHasMinContentMeasureFuncJNI(nativePointer, measureFunction != null) diff --git a/packages/react-native/ReactAndroid/src/main/jni/first-party/yogajni/jni/YGJNIVanilla.cpp b/packages/react-native/ReactAndroid/src/main/jni/first-party/yogajni/jni/YGJNIVanilla.cpp index f720c2084062..d3d39a704c17 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/first-party/yogajni/jni/YGJNIVanilla.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/first-party/yogajni/jni/YGJNIVanilla.cpp @@ -685,6 +685,15 @@ static void jni_YGNodeSetHasMeasureFuncJNI( static_cast(hasMeasureFunc) ? YGJNIMeasureFunc : nullptr); } +static void jni_YGNodeSetNodeTypeJNI( + JNIEnv* /*env*/, + jobject /*obj*/, + jlong nativePointer, + jint nodeType) { + YGNodeSetNodeType( + _jlong2YGNodeRef(nativePointer), static_cast(nodeType)); +} + static YGSize YGJNIMinContentMeasureFunc( YGNodeConstRef node, float width, @@ -1139,6 +1148,7 @@ static JNINativeMethod methods[] = { "(JZ)V", (void*)jni_YGNodeSetHasMeasureFuncJNI}, // NOLINTBEGIN(cppcoreguidelines-pro-type-cstyle-cast) + {"jni_YGNodeSetNodeTypeJNI", "(JI)V", (void*)jni_YGNodeSetNodeTypeJNI}, {"jni_YGNodeSetHasMinContentMeasureFuncJNI", "(JZ)V", (void*)jni_YGNodeSetHasMinContentMeasureFuncJNI},