diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 44dd99f96037..2ba10f484c5d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -522,6 +522,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun skipUnchangedChildrenInDifferentiator(): Boolean = accessor.skipUnchangedChildrenInDifferentiator() + /** + * Layout keeps the overflowInset of a node from its previous revision, instead of computing it again from its children, when neither the node nor its children changed. + */ + @JvmStatic + public fun skipUnchangedOverflowInsetInLayout(): Boolean = accessor.skipUnchangedOverflowInsetInLayout() + /** * Override getClipBounds on Android views to return the padding box when overflow is hidden */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 1279cf31c234..9f875c054711 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<4bcfd7705680a00615bddc6ffeccfe4b>> + * @generated SignedSource<> */ /** @@ -102,6 +102,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var shouldTriggerResponderTransferOnScrollAndroidCache: Boolean? = null private var skipActivityIdentityAssertionOnHostPauseCache: Boolean? = null private var skipUnchangedChildrenInDifferentiatorCache: Boolean? = null + private var skipUnchangedOverflowInsetInLayoutCache: Boolean? = null private var syncAndroidClipBoundsWithOverflowCache: Boolean? = null private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null private var updateRuntimeShadowNodeReferencesOnCommitCache: Boolean? = null @@ -857,6 +858,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun skipUnchangedOverflowInsetInLayout(): Boolean { + var cached = skipUnchangedOverflowInsetInLayoutCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.skipUnchangedOverflowInsetInLayout() + skipUnchangedOverflowInsetInLayoutCache = cached + } + return cached + } + override fun syncAndroidClipBoundsWithOverflow(): Boolean { var cached = syncAndroidClipBoundsWithOverflowCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 761628977aea..39fd0b30e150 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<358f98df922b0d9dbd89d8f8e903ac29>> */ /** @@ -192,6 +192,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun skipUnchangedChildrenInDifferentiator(): Boolean + @DoNotStrip @JvmStatic public external fun skipUnchangedOverflowInsetInLayout(): Boolean + @DoNotStrip @JvmStatic public external fun syncAndroidClipBoundsWithOverflow(): Boolean @DoNotStrip @JvmStatic public external fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index 00f27314aa41..909d685e43c5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<97ba66f8a849c88d77ee4b65a1997129>> */ /** @@ -187,6 +187,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun skipUnchangedChildrenInDifferentiator(): Boolean = false + override fun skipUnchangedOverflowInsetInLayout(): Boolean = false + override fun syncAndroidClipBoundsWithOverflow(): Boolean = false override fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 2a4a3159e7b3..7e1a3d5dabb5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<4e53ce712d5e472d8a9dfa673a108aa5>> + * @generated SignedSource<> */ /** @@ -106,6 +106,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var shouldTriggerResponderTransferOnScrollAndroidCache: Boolean? = null private var skipActivityIdentityAssertionOnHostPauseCache: Boolean? = null private var skipUnchangedChildrenInDifferentiatorCache: Boolean? = null + private var skipUnchangedOverflowInsetInLayoutCache: Boolean? = null private var syncAndroidClipBoundsWithOverflowCache: Boolean? = null private var traceTurboModulePromiseRejectionsOnAndroidCache: Boolean? = null private var updateRuntimeShadowNodeReferencesOnCommitCache: Boolean? = null @@ -943,6 +944,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun skipUnchangedOverflowInsetInLayout(): Boolean { + var cached = skipUnchangedOverflowInsetInLayoutCache + if (cached == null) { + cached = currentProvider.skipUnchangedOverflowInsetInLayout() + accessedFeatureFlags.add("skipUnchangedOverflowInsetInLayout") + skipUnchangedOverflowInsetInLayoutCache = cached + } + return cached + } + override fun syncAndroidClipBoundsWithOverflow(): Boolean { var cached = syncAndroidClipBoundsWithOverflowCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 02cc68b86d2d..2be0178d58dc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<44add928aa34871a70b7f35524ddfd97>> */ /** @@ -187,6 +187,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun skipUnchangedChildrenInDifferentiator(): Boolean + @DoNotStrip public fun skipUnchangedOverflowInsetInLayout(): Boolean + @DoNotStrip public fun syncAndroidClipBoundsWithOverflow(): Boolean @DoNotStrip public fun traceTurboModulePromiseRejectionsOnAndroid(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 679c83b3f217..efa4b3517d42 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<80dd5355f9f95d594f04d6b41fc785f5>> + * @generated SignedSource<> */ /** @@ -531,6 +531,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool skipUnchangedOverflowInsetInLayout() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("skipUnchangedOverflowInsetInLayout"); + return method(javaProvider_); + } + bool syncAndroidClipBoundsWithOverflow() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("syncAndroidClipBoundsWithOverflow"); @@ -1041,6 +1047,11 @@ bool JReactNativeFeatureFlagsCxxInterop::skipUnchangedChildrenInDifferentiator( return ReactNativeFeatureFlags::skipUnchangedChildrenInDifferentiator(); } +bool JReactNativeFeatureFlagsCxxInterop::skipUnchangedOverflowInsetInLayout( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::skipUnchangedOverflowInsetInLayout(); +} + bool JReactNativeFeatureFlagsCxxInterop::syncAndroidClipBoundsWithOverflow( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::syncAndroidClipBoundsWithOverflow(); @@ -1398,6 +1409,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "skipUnchangedChildrenInDifferentiator", JReactNativeFeatureFlagsCxxInterop::skipUnchangedChildrenInDifferentiator), + makeNativeMethod( + "skipUnchangedOverflowInsetInLayout", + JReactNativeFeatureFlagsCxxInterop::skipUnchangedOverflowInsetInLayout), makeNativeMethod( "syncAndroidClipBoundsWithOverflow", JReactNativeFeatureFlagsCxxInterop::syncAndroidClipBoundsWithOverflow), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index 9937f7e11356..5c177ed8d35f 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5fcb0abec30cc84df7a627d008f6c92a>> + * @generated SignedSource<<6d35a981b8b0d5c65c478e80ae6cca93>> */ /** @@ -276,6 +276,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool skipUnchangedChildrenInDifferentiator( facebook::jni::alias_ref); + static bool skipUnchangedOverflowInsetInLayout( + facebook::jni::alias_ref); + static bool syncAndroidClipBoundsWithOverflow( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 6e693f790d11..e42dae6d42c0 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<9b64fa4c8329098d9c2977ad2a162fbf>> + * @generated SignedSource<<2f8d7b1943ee966572fa7e0ab4354680>> */ /** @@ -354,6 +354,10 @@ bool ReactNativeFeatureFlags::skipUnchangedChildrenInDifferentiator() { return getAccessor().skipUnchangedChildrenInDifferentiator(); } +bool ReactNativeFeatureFlags::skipUnchangedOverflowInsetInLayout() { + return getAccessor().skipUnchangedOverflowInsetInLayout(); +} + bool ReactNativeFeatureFlags::syncAndroidClipBoundsWithOverflow() { return getAccessor().syncAndroidClipBoundsWithOverflow(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 6f4e6c34c75c..50b8b6307011 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<0a8759cd09aa452417aaa3720da346ae>> + * @generated SignedSource<<2e63151422c43c51fe760dec4379bb73>> */ /** @@ -451,6 +451,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool skipUnchangedChildrenInDifferentiator(); + /** + * Layout keeps the overflowInset of a node from its previous revision, instead of computing it again from its children, when neither the node nor its children changed. + */ + RN_EXPORT static bool skipUnchangedOverflowInsetInLayout(); + /** * Override getClipBounds on Android views to return the padding box when overflow is hidden */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index e948ff262bab..d72ebbeec69c 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<91748c760bda190e9c2f328150d9eb11>> + * @generated SignedSource<<1a408f815dd1e73b7f01276b5fb6862a>> */ /** @@ -1505,6 +1505,24 @@ bool ReactNativeFeatureFlagsAccessor::skipUnchangedChildrenInDifferentiator() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::skipUnchangedOverflowInsetInLayout() { + auto flagValue = skipUnchangedOverflowInsetInLayout_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(82, "skipUnchangedOverflowInsetInLayout"); + + flagValue = currentProvider_->skipUnchangedOverflowInsetInLayout(); + skipUnchangedOverflowInsetInLayout_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::syncAndroidClipBoundsWithOverflow() { auto flagValue = syncAndroidClipBoundsWithOverflow_.load(); @@ -1514,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipBoundsWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "syncAndroidClipBoundsWithOverflow"); + markFlagAsAccessed(83, "syncAndroidClipBoundsWithOverflow"); flagValue = currentProvider_->syncAndroidClipBoundsWithOverflow(); syncAndroidClipBoundsWithOverflow_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(84, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(85, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(86, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1586,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(87, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1604,7 +1622,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "useFabricInterop"); + markFlagAsAccessed(88, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1622,7 +1640,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(89, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1640,7 +1658,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(89, "useNestedScrollViewAndroid"); + markFlagAsAccessed(90, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1658,7 +1676,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(90, "useSharedAnimatedBackend"); + markFlagAsAccessed(91, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1676,7 +1694,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(91, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(92, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1694,7 +1712,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(92, "useTurboModuleInterop"); + markFlagAsAccessed(93, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1712,7 +1730,7 @@ bool ReactNativeFeatureFlagsAccessor::useWindowDensityForDisplayMetricsAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(93, "useWindowDensityForDisplayMetricsAndroid"); + markFlagAsAccessed(94, "useWindowDensityForDisplayMetricsAndroid"); flagValue = currentProvider_->useWindowDensityForDisplayMetricsAndroid(); useWindowDensityForDisplayMetricsAndroid_ = flagValue; @@ -1730,7 +1748,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(94, "viewCullingOutsetRatio"); + markFlagAsAccessed(95, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1748,7 +1766,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(95, "viewTransitionEnabled"); + markFlagAsAccessed(96, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1766,7 +1784,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionUseHardwareBitmapAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(96, "viewTransitionUseHardwareBitmapAndroid"); + markFlagAsAccessed(97, "viewTransitionUseHardwareBitmapAndroid"); flagValue = currentProvider_->viewTransitionUseHardwareBitmapAndroid(); viewTransitionUseHardwareBitmapAndroid_ = flagValue; @@ -1784,7 +1802,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(97, "virtualViewPrerenderRatio"); + markFlagAsAccessed(98, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 15bf6a4a1932..f2fe82e2d294 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<782d0e42e0854fc260d458a29ea52b8f>> + * @generated SignedSource<> */ /** @@ -116,6 +116,7 @@ class ReactNativeFeatureFlagsAccessor { bool shouldTriggerResponderTransferOnScrollAndroid(); bool skipActivityIdentityAssertionOnHostPause(); bool skipUnchangedChildrenInDifferentiator(); + bool skipUnchangedOverflowInsetInLayout(); bool syncAndroidClipBoundsWithOverflow(); bool traceTurboModulePromiseRejectionsOnAndroid(); bool updateRuntimeShadowNodeReferencesOnCommit(); @@ -143,7 +144,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 98> accessedFeatureFlags_; + std::array, 99> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -227,6 +228,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> shouldTriggerResponderTransferOnScrollAndroid_; std::atomic> skipActivityIdentityAssertionOnHostPause_; std::atomic> skipUnchangedChildrenInDifferentiator_; + std::atomic> skipUnchangedOverflowInsetInLayout_; std::atomic> syncAndroidClipBoundsWithOverflow_; std::atomic> traceTurboModulePromiseRejectionsOnAndroid_; std::atomic> updateRuntimeShadowNodeReferencesOnCommit_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 3851715ea2ee..80540b28540a 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -357,6 +357,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool skipUnchangedOverflowInsetInLayout() override { + return false; + } + bool syncAndroidClipBoundsWithOverflow() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index ea254225f50a..dd4d889319d6 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<3e619f92c56f90a6ff3eea0c48d027a7>> + * @generated SignedSource<<102745c7905cf47d85a19c30e50bf8a9>> */ /** @@ -785,6 +785,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::skipUnchangedChildrenInDifferentiator(); } + bool skipUnchangedOverflowInsetInLayout() override { + auto value = values_["skipUnchangedOverflowInsetInLayout"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::skipUnchangedOverflowInsetInLayout(); + } + bool syncAndroidClipBoundsWithOverflow() override { auto value = values_["syncAndroidClipBoundsWithOverflow"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 7ff0b82af498..ac110fdb9c99 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<60437c0aafa9d1f8de97120056c42970>> + * @generated SignedSource<> */ /** @@ -109,6 +109,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool shouldTriggerResponderTransferOnScrollAndroid() = 0; virtual bool skipActivityIdentityAssertionOnHostPause() = 0; virtual bool skipUnchangedChildrenInDifferentiator() = 0; + virtual bool skipUnchangedOverflowInsetInLayout() = 0; virtual bool syncAndroidClipBoundsWithOverflow() = 0; virtual bool traceTurboModulePromiseRejectionsOnAndroid() = 0; virtual bool updateRuntimeShadowNodeReferencesOnCommit() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 9cf900769800..cafb3d18f09e 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -454,6 +454,11 @@ bool NativeReactNativeFeatureFlags::skipUnchangedChildrenInDifferentiator( return ReactNativeFeatureFlags::skipUnchangedChildrenInDifferentiator(); } +bool NativeReactNativeFeatureFlags::skipUnchangedOverflowInsetInLayout( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::skipUnchangedOverflowInsetInLayout(); +} + bool NativeReactNativeFeatureFlags::syncAndroidClipBoundsWithOverflow( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::syncAndroidClipBoundsWithOverflow(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index de0f076f3f95..3a43bb68bdf9 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -202,6 +202,8 @@ class NativeReactNativeFeatureFlags bool skipUnchangedChildrenInDifferentiator(jsi::Runtime& runtime); + bool skipUnchangedOverflowInsetInLayout(jsi::Runtime& runtime); + bool syncAndroidClipBoundsWithOverflow(jsi::Runtime& runtime); bool traceTurboModulePromiseRejectionsOnAndroid(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollViewShadowNode.cpp index 623f32874854..65e84e4b65c6 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/scrollview/ScrollViewShadowNode.cpp @@ -43,6 +43,8 @@ void ScrollViewShadowNode::updateScrollContentOffsetIfNeeded() { if (layoutMetrics.frame.origin.x != 0) { layoutMetrics.frame.origin.x = 0; layoutableNode->setLayoutMetrics(layoutMetrics); + // The next layout has to compute `overflowInset` with this origin. + overflowInsetNeedsUpdate_ = true; } } } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp index cca684c33824..544f69bffe81 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.cpp @@ -136,6 +136,9 @@ YogaLayoutableShadowNode::YogaLayoutableShadowNode( yogaTreeHasBeenConfigured_ = static_cast(sourceShadowNode) .yogaTreeHasBeenConfigured_; + overflowInsetNeedsUpdate_ = + static_cast(sourceShadowNode) + .overflowInsetNeedsUpdate_; } if (fragment.props) { @@ -227,6 +230,7 @@ void YogaLayoutableShadowNode::appendChild( // Calling the base class (`ShadowNode`) method. LayoutableShadowNode::appendChild(childNode); + overflowInsetNeedsUpdate_ = true; if (getTraits().check(ShadowNodeTraits::Trait::LeafYogaNode)) { // This node is a declared leaf. @@ -269,6 +273,16 @@ void YogaLayoutableShadowNode::replaceChild( auto layoutableNewChild = std::dynamic_pointer_cast(newChild); + // A clone made by Yoga's clone-on-write (`adoptYogaChild()` and + // `cloneChildInPlace()`) has the same props and layout metrics, so it doesn't + // change the content bounds of this node. Any other replacement can. + if (layoutableOldChild == nullptr || layoutableNewChild == nullptr || + oldChild.getProps() != newChild->getProps() || + layoutableOldChild->layoutMetrics_ != + layoutableNewChild->layoutMetrics_) { + overflowInsetNeedsUpdate_ = true; + } + if (layoutableOldChild == nullptr && layoutableNewChild == nullptr) { // No need to mutate yogaLayoutableChildren_ return; @@ -741,6 +755,9 @@ void YogaLayoutableShadowNode::layoutTree( layoutMetrics.pointScaleFactor = layoutContext.pointScaleFactor; layoutMetrics.fontSizeMultiplier = layoutContext.fontSizeMultiplier; layoutMetrics.wasLeftAndRightSwapped = swapLeftAndRight; + // The metrics of a root can also be set from outside, like those of a + // clipped text attachment, so its `overflowInset` is always computed again. + overflowInsetNeedsUpdate_ = true; setLayoutMetrics(layoutMetrics); yogaNode_.setHasNewLayout(false); } @@ -766,6 +783,9 @@ void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) { // Reading data from a dirtied node does not make sense. react_native_assert(!YGNodeIsDirty(&yogaNode_)); + const bool skipUnchangedOverflowInset = + ReactNativeFeatureFlags::skipUnchangedOverflowInsetInLayout(); + bool anyChildHasNewLayout = false; for (auto childYogaNode : yogaNode_.getChildren()) { auto& childNode = shadowNodeFromContext(childYogaNode); @@ -773,6 +793,7 @@ void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) { react_native_assert(&childNode.yogaNode_ == childYogaNode); if (childYogaNode->getHasNewLayout()) { + anyChildHasNewLayout = true; childYogaNode->setHasNewLayout(false); // Reading data from a dirtied node does not make sense. @@ -803,6 +824,13 @@ void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) { layoutContext.affectedNodes->push_back(&childNode); } + if (skipUnchangedOverflowInset && + newLayoutMetrics.displayType != DisplayType::None) { + // The child's `layout()` below computes `overflowInset` again when it + // can have changed. + newLayoutMetrics.overflowInset = childNode.layoutMetrics_.overflowInset; + } + childNode.setLayoutMetrics(newLayoutMetrics); if (newLayoutMetrics.displayType != DisplayType::None) { @@ -811,7 +839,16 @@ void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) { } } - if (YGNodeStyleGetOverflow(&yogaNode_) == YGOverflowVisible) { + if (YGNodeStyleGetOverflow(&yogaNode_) != YGOverflowVisible) { + layoutMetrics_.overflowInset = {}; + } else if ( + !skipUnchangedOverflowInset || overflowInsetNeedsUpdate_ || + anyChildHasNewLayout) { + // Yoga also sets `hasNewLayout` on a node whose layout it takes from its + // cache, without laying out its children again. So the content bounds are + // computed again only when a child has a new layout or something else + // they depend on changed. + // // Note that the parent node's overflow layout is NOT affected by its // transform matrix. That transform matrix is applied on the parent node // as well as all of its child nodes, which won't cause changes on the @@ -823,9 +860,8 @@ void YogaLayoutableShadowNode::layout(LayoutContext layoutContext) { auto contentBounds = getContentBounds(); layoutMetrics_.overflowInset = calculateOverflowInset(layoutMetrics_.frame, contentBounds); - } else { - layoutMetrics_.overflowInset = {}; } + overflowInsetNeedsUpdate_ = false; } Rect YogaLayoutableShadowNode::getContentBounds() const { diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h index 47edf4e9e4ad..1a73e5fa5c4c 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/YogaLayoutableShadowNode.h @@ -109,6 +109,15 @@ class YogaLayoutableShadowNode : public LayoutableShadowNode { */ mutable yoga::Node yogaNode_; + /* + * Whether `layout()` has to compute `overflowInset` again because the props + * or the children of this node changed since its last layout. Children with + * a new layout, which include all children when the size of this node + * changes, are checked in `layout()` itself. Subclasses that change the + * layout metrics of their children after `layout()` have to set it. + */ + bool overflowInsetNeedsUpdate_{true}; + private: void updateYogaChildrenOwnersIfNeeded(); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/tests/OverflowInsetTest.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/tests/OverflowInsetTest.cpp new file mode 100644 index 000000000000..e29a0e1b46f8 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/tests/OverflowInsetTest.cpp @@ -0,0 +1,397 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace facebook::react { + +namespace { + +// The props of the tree below that the tests change. +struct TreeParams { + float containerWidth{50}; + float overflowingChildLeft{70}; + bool clipOverflowingChildParent{false}; + bool translateChild{false}; + EdgeInsets siblingHitSlop{}; + float movingSiblingTop{0}; + float otherContainerTop{100}; +}; + +constexpr Tag kRootTag = 1; +constexpr Tag kContainerTag = 2; +constexpr Tag kChildTag = 3; +constexpr Tag kOverflowingChildParentTag = 4; +constexpr Tag kOverflowingChildTag = 5; +constexpr Tag kSiblingTag = 6; +// Moving this container lays out the root again while the layout of the other +// container comes from Yoga's cache, so its children don't get a new layout. +constexpr Tag kOtherContainerTag = 7; +constexpr Tag kFirstRowTag = 10; +constexpr int kRowCount = 10; +constexpr Tag kMovingRowTag = kFirstRowTag + 5; + +std::shared_ptr +absoluteViewProps(float left, float top, float width, float height) { + auto props = std::make_shared(); + auto& yogaStyle = props->yogaStyle; + yogaStyle.setPositionType(yoga::PositionType::Absolute); + yogaStyle.setPosition(yoga::Edge::Left, yoga::StyleLength::points(left)); + yogaStyle.setPosition(yoga::Edge::Top, yoga::StyleLength::points(top)); + yogaStyle.setDimension( + yoga::Dimension::Width, yoga::StyleSizeLength::points(width)); + yogaStyle.setDimension( + yoga::Dimension::Height, yoga::StyleSizeLength::points(height)); + return props; +} + +std::shared_ptr sizedViewProps(float width, float height) { + auto props = std::make_shared(); + props->yogaStyle.setDimension( + yoga::Dimension::Width, yoga::StyleSizeLength::points(width)); + props->yogaStyle.setDimension( + yoga::Dimension::Height, yoga::StyleSizeLength::points(height)); + return props; +} + +std::shared_ptr propsForTag(Tag tag, const TreeParams& params) { + switch (tag) { + case kContainerTag: + return absoluteViewProps(0, 0, params.containerWidth, 50); + case kOtherContainerTag: + return absoluteViewProps(0, params.otherContainerTop, 40, 40); + case kChildTag: { + auto props = absoluteViewProps(10, 10, 30, 90); + if (params.translateChild) { + props->transform = Transform::Translate(10, 10, 0); + } + return props; + } + case kOverflowingChildParentTag: { + auto props = absoluteViewProps(10, 10, 110, 20); + if (params.clipOverflowingChildParent) { + props->yogaStyle.setOverflow(yoga::Overflow::Hidden); + } + return props; + } + case kOverflowingChildTag: + return absoluteViewProps(params.overflowingChildLeft, -50, 30, 60); + case kSiblingTag: { + auto props = absoluteViewProps(-60, 50, 70, 20); + props->hitSlop = params.siblingHitSlop; + return props; + } + default: + break; + } + if (tag >= kFirstRowTag && tag < kFirstRowTag + kRowCount) { + // Rows of the container, each with a child that overflows it. + auto top = tag == kMovingRowTag ? params.movingSiblingTop + : static_cast(tag - kFirstRowTag); + return absoluteViewProps(0, top * 5, 20, 5); + } + // The children of the rows. + return absoluteViewProps(15, 0, 20, 5); +} + +std::shared_ptr buildTree( + ComponentBuilder& builder, + const TreeParams& params) { + std::shared_ptr rootShadowNode; + + std::vector containerChildren; + containerChildren.push_back( + Element() + .tag(kChildTag) + .props(propsForTag(kChildTag, params)) + .children({Element() + .tag(kOverflowingChildParentTag) + .props(propsForTag(kOverflowingChildParentTag, params)) + .children({Element() + .tag(kOverflowingChildTag) + .props(propsForTag( + kOverflowingChildTag, params))})})); + containerChildren.push_back( + Element() + .tag(kSiblingTag) + .props(propsForTag(kSiblingTag, params))); + for (Tag rowTag = kFirstRowTag; rowTag < kFirstRowTag + kRowCount; rowTag++) { + auto rowChildTag = rowTag + 100; + containerChildren.push_back( + Element() + .tag(rowTag) + .props(propsForTag(rowTag, params)) + .children({Element() + .tag(rowChildTag) + .props(propsForTag(rowChildTag, params))})); + } + + auto element = + Element() + .reference(rootShadowNode) + .tag(kRootTag) + .props([] { + auto props = std::make_shared(); + props->layoutConstraints = LayoutConstraints{ + .minimumSize = {.width = 0, .height = 0}, + .maximumSize = {.width = 500, .height = 500}}; + props->yogaStyle.setDimension( + yoga::Dimension::Width, yoga::StyleSizeLength::points(200)); + props->yogaStyle.setDimension( + yoga::Dimension::Height, yoga::StyleSizeLength::points(200)); + return props; + }) + .children( + {Element() + .tag(kContainerTag) + .props(propsForTag(kContainerTag, params)) + .children(std::move(containerChildren)), + Element() + .tag(kOtherContainerTag) + .props(propsForTag(kOtherContainerTag, params))}); + + builder.build(element); + rootShadowNode->layoutIfNeeded(); + return rootShadowNode; +} + +const ShadowNode* findByTag(const ShadowNode& node, Tag tag) { + if (node.getTag() == tag) { + return &node; + } + for (const auto& child : node.getChildren()) { + if (auto found = findByTag(*child, tag)) { + return found; + } + } + return nullptr; +} + +// Expects every node of `actual` to have the frame and overflowInset of the +// node with the same tag in `expected`. +void expectSameLayout(const ShadowNode& actual, const ShadowNode& expected) { + const auto& actualMetrics = + dynamic_cast(actual).getLayoutMetrics(); + const auto& expectedMetrics = + dynamic_cast(expected).getLayoutMetrics(); + EXPECT_EQ(actualMetrics.frame, expectedMetrics.frame) + << "frame of tag " << actual.getTag(); + EXPECT_EQ(actualMetrics.overflowInset, expectedMetrics.overflowInset) + << "overflowInset of tag " << actual.getTag(); + ASSERT_EQ(actual.getChildren().size(), expected.getChildren().size()); + for (size_t i = 0; i < actual.getChildren().size(); i++) { + expectSameLayout(*actual.getChildren()[i], *expected.getChildren()[i]); + } +} + +// Expects the overflowInset of every node with `overflow: visible` to be the +// one its children give now. +void expectOverflowInsetFromChildren(const ShadowNode& node) { + const auto* viewProps = dynamic_cast(node.getProps().get()); + const auto* layoutableNode = + dynamic_cast(&node); + if (viewProps != nullptr && layoutableNode != nullptr && + viewProps->yogaStyle.overflow() == yoga::Overflow::Visible) { + auto metrics = layoutableNode->getLayoutMetrics(); + auto bounds = layoutableNode->getContentBounds(); + auto expected = EdgeInsets{ + .left = std::min(bounds.getMinX(), Float{0}), + .top = std::min(bounds.getMinY(), Float{0}), + .right = + -std::max(bounds.getMaxX() - metrics.frame.size.width, Float{0}), + .bottom = + -std::max(bounds.getMaxY() - metrics.frame.size.height, Float{0})}; + EXPECT_EQ(metrics.overflowInset, expected) + << "overflowInset of tag " << node.getTag(); + } + for (const auto& child : node.getChildren()) { + expectOverflowInsetFromChildren(*child); + } +} + +class SkipUnchangedOverflowInsetFlags : public ReactNativeFeatureFlagsDefaults { + public: + bool skipUnchangedOverflowInsetInLayout() override { + return true; + } +}; + +// Runs each test with `skipUnchangedOverflowInsetInLayout` off and on. +class OverflowInsetTest : public ::testing::TestWithParam { + protected: + void SetUp() override { + ReactNativeFeatureFlags::dangerouslyReset(); + if (GetParam()) { + ReactNativeFeatureFlags::override( + std::make_unique()); + } + } + + void TearDown() override { + ReactNativeFeatureFlags::dangerouslyReset(); + } +}; + +} // namespace + +// Each change is committed on top of the previous revision, which carries the +// overflowInset of nodes over from the previous layout. The result has to be +// the same as laying out a tree built with the final props from scratch. +TEST_P(OverflowInsetTest, overflowInsetMatchesFreshLayoutAfterEachRevision) { + auto builder = simpleComponentBuilder(); + + struct Change { + const char* name; + std::vector tags; + std::function apply; + }; + std::vector changes = { + {"move the overflowing child", + {kOverflowingChildTag}, + [](TreeParams& p) { p.overflowingChildLeft = 20; }}, + {"add hitSlop to a sibling while something else moves", + {kSiblingTag, kOtherContainerTag}, + [](TreeParams& p) { + p.siblingHitSlop = EdgeInsets{50, 50, 50, 50}; + p.otherContainerTop = 110; + }}, + {"translate a child while something else moves", + {kChildTag, kOtherContainerTag}, + [](TreeParams& p) { + p.translateChild = true; + p.otherContainerTop = 120; + }}, + {"resize the container", + {kContainerTag}, + [](TreeParams& p) { p.containerWidth = 120; }}, + {"clip the overflowing child", + {kOverflowingChildParentTag}, + [](TreeParams& p) { p.clipOverflowingChildParent = true; }}, + {"unclip, translate a child and move the overflowing child", + {kChildTag, kOverflowingChildParentTag, kOverflowingChildTag}, + [](TreeParams& p) { + p.clipOverflowingChildParent = false; + p.translateChild = true; + p.overflowingChildLeft = 90; + }}, + {"add hitSlop to a sibling and move a row", + {kSiblingTag, kMovingRowTag}, + [](TreeParams& p) { + p.siblingHitSlop = EdgeInsets{50, 50, 50, 50}; + p.movingSiblingTop = 30; + }}, + {"move a row back", {kMovingRowTag}, [](TreeParams& p) { + p.movingSiblingTop = 5; + }}}; + + auto params = TreeParams{}; + auto rootShadowNode = buildTree(builder, params); + expectSameLayout(*rootShadowNode, *buildTree(builder, params)); + + for (const auto& change : changes) { + SCOPED_TRACE(change.name); + change.apply(params); + for (auto tag : change.tags) { + const auto* node = findByTag(*rootShadowNode, tag); + ASSERT_NE(node, nullptr); + rootShadowNode = + std::static_pointer_cast(rootShadowNode->cloneTree( + node->getFamily(), [&](const ShadowNode& oldShadowNode) { + return oldShadowNode.clone( + ShadowNodeFragment{.props = propsForTag(tag, params)}); + })); + } + rootShadowNode->layoutIfNeeded(); + expectSameLayout(*rootShadowNode, *buildTree(builder, params)); + expectOverflowInsetFromChildren(*rootShadowNode); + } +} + +// In a right-to-left layout, a ScrollView moves its content to x = 0 after +// its own layout. A later layout in which the ScrollView's layout comes from +// Yoga's cache has to compute its overflowInset with the moved content. +TEST_P(OverflowInsetTest, overflowInsetFollowsRightToLeftScrollViewContent) { + auto builder = simpleComponentBuilder(); + + auto buildRoot = [&](float siblingTop) { + std::shared_ptr rootShadowNode; + auto element = + Element() + .reference(rootShadowNode) + .tag(kRootTag) + .props([] { + auto props = std::make_shared(); + props->layoutConstraints = LayoutConstraints{ + .minimumSize = {.width = 0, .height = 0}, + .maximumSize = {.width = 500, .height = 500}, + .layoutDirection = LayoutDirection::RightToLeft}; + props->yogaStyle.setDimension( + yoga::Dimension::Width, yoga::StyleSizeLength::points(200)); + props->yogaStyle.setDimension( + yoga::Dimension::Height, yoga::StyleSizeLength::points(200)); + return props; + }) + .children( + {Element() + .tag(kContainerTag) + .props([] { + auto props = std::make_shared(); + props->yogaStyle.setDimension( + yoga::Dimension::Width, + yoga::StyleSizeLength::points(100)); + props->yogaStyle.setDimension( + yoga::Dimension::Height, + yoga::StyleSizeLength::points(50)); + return props; + }) + // Not positioned, so Yoga puts it at the right edge. + .children({Element().tag(kChildTag).props( + sizedViewProps(300, 50))}), + Element() + .tag(kSiblingTag) + .props(absoluteViewProps(0, siblingTop, 20, 20))}); + builder.build(element); + rootShadowNode->layoutIfNeeded(); + return rootShadowNode; + }; + + auto rootShadowNode = buildRoot(100); + const auto* sibling = findByTag(*rootShadowNode, kSiblingTag); + ASSERT_NE(sibling, nullptr); + rootShadowNode = + std::static_pointer_cast(rootShadowNode->cloneTree( + sibling->getFamily(), [&](const ShadowNode& oldShadowNode) { + return oldShadowNode.clone( + ShadowNodeFragment{.props = absoluteViewProps(0, 120, 20, 20)}); + })); + rootShadowNode->layoutIfNeeded(); + + expectOverflowInsetFromChildren(*rootShadowNode); +} + +INSTANTIATE_TEST_SUITE_P( + SkipUnchangedOverflowInset, + OverflowInsetTest, + ::testing::Bool(), + [](const ::testing::TestParamInfo& info) { + return info.param ? "FlagOn" : "FlagOff"; + }); + +} // namespace facebook::react diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index a1ee774b95b5..383a80259dc2 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -933,6 +933,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'none', }, + skipUnchangedOverflowInsetInLayout: { + defaultValue: false, + metadata: { + dateAdded: '2026-11-13', + description: + 'Layout keeps the overflowInset of a node from its previous revision, instead of computing it again from its children, when neither the node nor its children changed.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, syncAndroidClipBoundsWithOverflow: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 7f12ba56fe0d..9e234b475412 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<8817847943e654f2ce61fc39af75baf8>> + * @generated SignedSource<<7c7d18b932a5989207a461df2a9e5fc2>> * @flow strict * @noformat */ @@ -130,6 +130,7 @@ export type ReactNativeFeatureFlags = Readonly<{ shouldTriggerResponderTransferOnScrollAndroid: Getter, skipActivityIdentityAssertionOnHostPause: Getter, skipUnchangedChildrenInDifferentiator: Getter, + skipUnchangedOverflowInsetInLayout: Getter, syncAndroidClipBoundsWithOverflow: Getter, traceTurboModulePromiseRejectionsOnAndroid: Getter, updateRuntimeShadowNodeReferencesOnCommit: Getter, @@ -545,6 +546,10 @@ export const skipActivityIdentityAssertionOnHostPause: Getter = createN * When a node changes, the differ leaves out its children that are unchanged in both trees instead of describing and comparing them. */ export const skipUnchangedChildrenInDifferentiator: Getter = createNativeFlagGetter('skipUnchangedChildrenInDifferentiator', false); +/** + * Layout keeps the overflowInset of a node from its previous revision, instead of computing it again from its children, when neither the node nor its children changed. + */ +export const skipUnchangedOverflowInsetInLayout: Getter = createNativeFlagGetter('skipUnchangedOverflowInsetInLayout', false); /** * Override getClipBounds on Android views to return the padding box when overflow is hidden */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 1243b788a04f..37fc71cda888 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<009a693236af1c165a07259dd71088be>> * @flow strict * @noformat */ @@ -107,6 +107,7 @@ export interface Spec extends TurboModule { readonly shouldTriggerResponderTransferOnScrollAndroid?: () => boolean; readonly skipActivityIdentityAssertionOnHostPause?: () => boolean; readonly skipUnchangedChildrenInDifferentiator?: () => boolean; + readonly skipUnchangedOverflowInsetInLayout?: () => boolean; readonly syncAndroidClipBoundsWithOverflow?: () => boolean; readonly traceTurboModulePromiseRejectionsOnAndroid?: () => boolean; readonly updateRuntimeShadowNodeReferencesOnCommit?: () => boolean; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 912089515add..8b6972561f53 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -2560,6 +2560,7 @@ class facebook::react::VirtualViewProps : public facebook::react::HostPlatformVi } class facebook::react::YogaLayoutableShadowNode : public facebook::react::LayoutableShadowNode { + protected bool overflowInsetNeedsUpdate_; protected facebook::yoga::Config yogaConfig_; protected mutable facebook::yoga::Node yogaNode_; protected virtual bool shouldNewRevisionDirtyMeasurement(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment) const; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index 2567b4e72da9..5ad2b8e8b2e9 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -4409,6 +4409,7 @@ class facebook::react::VirtualViewProps : public facebook::react::HostPlatformVi } class facebook::react::YogaLayoutableShadowNode : public facebook::react::LayoutableShadowNode { + protected bool overflowInsetNeedsUpdate_; protected facebook::yoga::Config yogaConfig_; protected mutable facebook::yoga::Node yogaNode_; protected virtual bool shouldNewRevisionDirtyMeasurement(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment) const; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 34b1da7689bb..92aa8cbf71de 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -2553,6 +2553,7 @@ class facebook::react::VirtualViewProps : public facebook::react::HostPlatformVi } class facebook::react::YogaLayoutableShadowNode : public facebook::react::LayoutableShadowNode { + protected bool overflowInsetNeedsUpdate_; protected facebook::yoga::Config yogaConfig_; protected mutable facebook::yoga::Node yogaNode_; protected virtual bool shouldNewRevisionDirtyMeasurement(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment) const; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 34b1da7689bb..92aa8cbf71de 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -2553,6 +2553,7 @@ class facebook::react::VirtualViewProps : public facebook::react::HostPlatformVi } class facebook::react::YogaLayoutableShadowNode : public facebook::react::LayoutableShadowNode { + protected bool overflowInsetNeedsUpdate_; protected facebook::yoga::Config yogaConfig_; protected mutable facebook::yoga::Node yogaNode_; protected virtual bool shouldNewRevisionDirtyMeasurement(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment) const; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 65b1426db47e..e6edb602b776 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -4705,6 +4705,7 @@ class facebook::react::VirtualViewProps : public facebook::react::HostPlatformVi } class facebook::react::YogaLayoutableShadowNode : public facebook::react::LayoutableShadowNode { + protected bool overflowInsetNeedsUpdate_; protected facebook::yoga::Config yogaConfig_; protected mutable facebook::yoga::Node yogaNode_; protected virtual bool shouldNewRevisionDirtyMeasurement(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment) const; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index ec76e48de457..65a219e49db0 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -6528,6 +6528,7 @@ class facebook::react::VirtualViewProps : public facebook::react::HostPlatformVi } class facebook::react::YogaLayoutableShadowNode : public facebook::react::LayoutableShadowNode { + protected bool overflowInsetNeedsUpdate_; protected facebook::yoga::Config yogaConfig_; protected mutable facebook::yoga::Node yogaNode_; protected virtual bool shouldNewRevisionDirtyMeasurement(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment) const; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index c8c92eb83ed1..55db875f8dff 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -4691,6 +4691,7 @@ class facebook::react::VirtualViewProps : public facebook::react::HostPlatformVi } class facebook::react::YogaLayoutableShadowNode : public facebook::react::LayoutableShadowNode { + protected bool overflowInsetNeedsUpdate_; protected facebook::yoga::Config yogaConfig_; protected mutable facebook::yoga::Node yogaNode_; protected virtual bool shouldNewRevisionDirtyMeasurement(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment) const; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 3ebc82e69ba0..743bfa21fbec 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -4698,6 +4698,7 @@ class facebook::react::VirtualViewProps : public facebook::react::HostPlatformVi } class facebook::react::YogaLayoutableShadowNode : public facebook::react::LayoutableShadowNode { + protected bool overflowInsetNeedsUpdate_; protected facebook::yoga::Config yogaConfig_; protected mutable facebook::yoga::Node yogaNode_; protected virtual bool shouldNewRevisionDirtyMeasurement(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment) const; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 2ef9e05072ac..f9ca4468318f 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -1217,6 +1217,7 @@ class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShad } class facebook::react::YogaLayoutableShadowNode : public facebook::react::LayoutableShadowNode { + protected bool overflowInsetNeedsUpdate_; protected facebook::yoga::Config yogaConfig_; protected mutable facebook::yoga::Node yogaNode_; protected virtual bool shouldNewRevisionDirtyMeasurement(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment) const; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api index 58f5c38aa6a0..043d19868f0d 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api @@ -2828,6 +2828,7 @@ class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShad } class facebook::react::YogaLayoutableShadowNode : public facebook::react::LayoutableShadowNode { + protected bool overflowInsetNeedsUpdate_; protected facebook::yoga::Config yogaConfig_; protected mutable facebook::yoga::Node yogaNode_; protected virtual bool shouldNewRevisionDirtyMeasurement(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment) const; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 9279765a5038..def7d1a56e3a 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -1210,6 +1210,7 @@ class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShad } class facebook::react::YogaLayoutableShadowNode : public facebook::react::LayoutableShadowNode { + protected bool overflowInsetNeedsUpdate_; protected facebook::yoga::Config yogaConfig_; protected mutable facebook::yoga::Node yogaNode_; protected virtual bool shouldNewRevisionDirtyMeasurement(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment) const; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 9279765a5038..def7d1a56e3a 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -1210,6 +1210,7 @@ class facebook::react::ViewShadowNode : public facebook::react::ConcreteViewShad } class facebook::react::YogaLayoutableShadowNode : public facebook::react::LayoutableShadowNode { + protected bool overflowInsetNeedsUpdate_; protected facebook::yoga::Config yogaConfig_; protected mutable facebook::yoga::Node yogaNode_; protected virtual bool shouldNewRevisionDirtyMeasurement(const facebook::react::ShadowNode& sourceShadowNode, const facebook::react::ShadowNodeFragment& fragment) const;