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 368b64dca4e..0342cf267f2 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<> */ /** @@ -390,6 +390,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean = accessor.fixMappingOfEventPrioritiesBetweenFabricAndReact() + /** + * Re-measures text and re-sends padding and overflow insets to mounted views when the pixel density changes, so text and padded views do not keep sizes from the old density. + */ + @JvmStatic + public fun fixTextLayoutOnPixelDensityChange(): Boolean = accessor.fixTextLayoutOnPixelDensityChange() + /** * Fix flex basis computation to not apply FitContent constraint in the main axis for non-measure container nodes, preventing unnecessary re-measurement in scroll containers. */ 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 77d905c3393..bfc64b6a133 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<> + * @generated SignedSource<<0107593110db613063b6f4ceb50d8e58>> */ /** @@ -80,6 +80,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var enableVirtualViewContainerStateExperimentalCache: Boolean? = null private var fixBorderlessRippleAndroidCache: Boolean? = null private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null + private var fixTextLayoutOnPixelDensityChangeCache: Boolean? = null private var fixYogaFlexBasisFitContentInMainAxisCache: Boolean? = null private var fuseboxAssertSingleHostStateCache: Boolean? = null private var fuseboxEnabledReleaseCache: Boolean? = null @@ -653,6 +654,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun fixTextLayoutOnPixelDensityChange(): Boolean { + var cached = fixTextLayoutOnPixelDensityChangeCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.fixTextLayoutOnPixelDensityChange() + fixTextLayoutOnPixelDensityChangeCache = cached + } + return cached + } + override fun fixYogaFlexBasisFitContentInMainAxis(): Boolean { var cached = fixYogaFlexBasisFitContentInMainAxisCache 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 7f4ca76a9f1..93e68812842 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<<51b7baf7b3c2758ec619c77cb4fa637d>> + * @generated SignedSource<> */ /** @@ -148,6 +148,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean + @DoNotStrip @JvmStatic public external fun fixTextLayoutOnPixelDensityChange(): Boolean + @DoNotStrip @JvmStatic public external fun fixYogaFlexBasisFitContentInMainAxis(): Boolean @DoNotStrip @JvmStatic public external fun fuseboxAssertSingleHostState(): 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 5f784e44179..c593a036b4e 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<<768077a22c110aa98ab02bc96eec695b>> */ /** @@ -143,6 +143,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean = false + override fun fixTextLayoutOnPixelDensityChange(): Boolean = false + override fun fixYogaFlexBasisFitContentInMainAxis(): Boolean = false override fun fuseboxAssertSingleHostState(): Boolean = true 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 6ddcf3741e2..961e8c94ddc 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<<3e94c46a8a6fda2a4c0378e326291d73>> + * @generated SignedSource<<86ebc7307e7ebe2023e909a868ed3117>> */ /** @@ -84,6 +84,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var enableVirtualViewContainerStateExperimentalCache: Boolean? = null private var fixBorderlessRippleAndroidCache: Boolean? = null private var fixMappingOfEventPrioritiesBetweenFabricAndReactCache: Boolean? = null + private var fixTextLayoutOnPixelDensityChangeCache: Boolean? = null private var fixYogaFlexBasisFitContentInMainAxisCache: Boolean? = null private var fuseboxAssertSingleHostStateCache: Boolean? = null private var fuseboxEnabledReleaseCache: Boolean? = null @@ -717,6 +718,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun fixTextLayoutOnPixelDensityChange(): Boolean { + var cached = fixTextLayoutOnPixelDensityChangeCache + if (cached == null) { + cached = currentProvider.fixTextLayoutOnPixelDensityChange() + accessedFeatureFlags.add("fixTextLayoutOnPixelDensityChange") + fixTextLayoutOnPixelDensityChangeCache = cached + } + return cached + } + override fun fixYogaFlexBasisFitContentInMainAxis(): Boolean { var cached = fixYogaFlexBasisFitContentInMainAxisCache 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 7f67f443b3e..73121e261bb 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<<0c7d62679814835a3ef86a63174c7f7f>> + * @generated SignedSource<> */ /** @@ -143,6 +143,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun fixMappingOfEventPrioritiesBetweenFabricAndReact(): Boolean + @DoNotStrip public fun fixTextLayoutOnPixelDensityChange(): Boolean + @DoNotStrip public fun fixYogaFlexBasisFitContentInMainAxis(): Boolean @DoNotStrip public fun fuseboxAssertSingleHostState(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp index 837535e66d4..da3966e9c30 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp @@ -599,6 +599,8 @@ void FabricMountingManager::executeMount( bool maintainMutationOrder = ReactNativeFeatureFlags::disableMountItemReorderingAndroid(); + bool updateViewsOnDensityChange = + ReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange(); auto revisionNumber = telemetry.getRevisionNumber(); @@ -688,12 +690,19 @@ void FabricMountingManager::executeMount( CppMountItem::UpdateStateMountItem(newChildShadowView)); } + bool pointScaleChanged = updateViewsOnDensityChange && + oldChildShadowView.layoutMetrics.pointScaleFactor != + newChildShadowView.layoutMetrics.pointScaleFactor; + // Padding: padding mountItems must be executed before layout props // are updated in the view. This is necessary to ensure that events // (resulting from layout changes) are dispatched with the correct // padding information. if (oldChildShadowView.layoutMetrics.contentInsets != - newChildShadowView.layoutMetrics.contentInsets) { + newChildShadowView.layoutMetrics.contentInsets || + (pointScaleChanged && + newChildShadowView.layoutMetrics.contentInsets != + EdgeInsets::ZERO)) { (maintainMutationOrder ? cppCommonMountItems : cppUpdatePaddingMountItems) .push_back( @@ -713,8 +722,11 @@ void FabricMountingManager::executeMount( // children of the current view. The layout of current view may not // change, and we separate this part from layout mount items to not // pack too much data there. - if ((oldChildShadowView.layoutMetrics.overflowInset != - newChildShadowView.layoutMetrics.overflowInset)) { + if (oldChildShadowView.layoutMetrics.overflowInset != + newChildShadowView.layoutMetrics.overflowInset || + (pointScaleChanged && + newChildShadowView.layoutMetrics.overflowInset != + EdgeInsets::ZERO)) { (maintainMutationOrder ? cppCommonMountItems : cppUpdateOverflowInsetMountItems) .push_back( diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test_helper/FabricMountingManagerTestHelper.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test_helper/FabricMountingManagerTestHelper.cpp index 473316f6208..8a72bfa3ab7 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test_helper/FabricMountingManagerTestHelper.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test_helper/FabricMountingManagerTestHelper.cpp @@ -7,24 +7,60 @@ #include "FabricMountingManagerTestHelper.h" +#include +#include #include #include #include #include +#include +#include #include namespace facebook::react { +static LayoutConstraints makeLayoutConstraints( + jfloat widthDp, + jfloat heightDp) { + Size size{.width = widthDp, .height = heightDp}; + return LayoutConstraints{ + .minimumSize = size, + .maximumSize = size, + .layoutDirection = LayoutDirection::LeftToRight}; +} + FabricMountingManagerTestHelper::FabricMountingManagerTestHelper( jni::alias_ref jFabricUIManager) : javaUIManager_(jni::make_global(jFabricUIManager)) { mountingManager_ = std::make_shared(javaUIManager_); + contextContainer_ = std::make_shared(); auto params = ComponentDescriptorParameters{ .eventDispatcher = EventDispatcher::Shared{}, - .contextContainer = std::make_shared(), + .contextContainer = contextContainer_, .flavor = nullptr}; viewComponentDescriptor_ = std::make_unique(params); + + class TestShadowTreeDelegate : public ShadowTreeDelegate { + public: + RootShadowNode::Unshared shadowTreeWillCommit( + [[maybe_unused]] const ShadowTree& shadowTree, + [[maybe_unused]] const RootShadowNode::Shared& oldRootShadowNode, + const RootShadowNode::Unshared& newRootShadowNode, + [[maybe_unused]] const ShadowTreeCommitOptions& commitOptions) + const override { + return newRootShadowNode; + } + + void shadowTreeDidFinishTransaction( + [[maybe_unused]] std::shared_ptr + mountingCoordinator, + [[maybe_unused]] bool mountSynchronously) const override {} + + void shadowTreeDidFinishReactCommit( + [[maybe_unused]] const ShadowTree& shadowTree) const override {} + }; + shadowTreeDelegate_ = std::make_unique(); } void FabricMountingManagerTestHelper::initHybrid( @@ -81,6 +117,112 @@ void FabricMountingManagerTestHelper::synchronouslyUpdateAnimatedProps( mountingManager_->synchronouslyUpdateAnimatedProps(updates); } +void FabricMountingManagerTestHelper::startSurfaceWithConstraints( + jint surfaceId, + jfloat widthDp, + jfloat heightDp, + jfloat pointScaleFactor) { + shadowTree_ = std::make_unique( + surfaceId, + makeLayoutConstraints(widthDp, heightDp), + LayoutContext{.pointScaleFactor = pointScaleFactor}, + *shadowTreeDelegate_, + *contextContainer_); + mountingManager_->onSurfaceStart(surfaceId); +} + +void FabricMountingManagerTestHelper::mountViewWithPadding( + jint surfaceId, + jint tag, + jfloat padding) { + using ShadowNodeList = std::vector>; + PropsParserContext parserContext{surfaceId, *contextContainer_}; + auto createView = [&](Tag viewTag, + folly::dynamic style, + ShadowNodeList children) { + auto family = viewComponentDescriptor_->createFamily( + {.tag = viewTag, .surfaceId = surfaceId, .instanceHandle = nullptr}); + auto props = viewComponentDescriptor_->cloneProps( + parserContext, nullptr, RawProps(std::move(style))); + return viewComponentDescriptor_->createShadowNode( + {.props = props, + .children = + std::make_shared(std::move(children)), + .state = viewComponentDescriptor_->createInitialState(props, family)}, + family); + }; + + auto overflowingChild = createView( + tag + 1, + folly::dynamic::object("collapsable", false)("position", "absolute")( + "left", -padding)("top", -padding)("width", 100 + 2 * padding)( + "height", 100 + 2 * padding), + {}); + auto paddedView = createView( + tag, + folly::dynamic::object("collapsable", false)("width", 100)("height", 100)( + "padding", padding), + {overflowingChild}); + + shadowTree_->commit( + [&](const RootShadowNode& oldRootShadowNode) { + return std::static_pointer_cast( + oldRootShadowNode.ShadowNode::clone( + {.children = std::make_shared( + ShadowNodeList{paddedView})})); + }, + {}); + mountPendingTransaction(); +} + +void FabricMountingManagerTestHelper::updateSurfaceConstraints( + jint surfaceId, + jfloat widthDp, + jfloat heightDp, + jfloat pointScaleFactor) { + PropsParserContext parserContext{surfaceId, *contextContainer_}; + shadowTree_->commit( + [&](const RootShadowNode& oldRootShadowNode) { + return oldRootShadowNode.clone( + parserContext, + makeLayoutConstraints(widthDp, heightDp), + LayoutContext{.pointScaleFactor = pointScaleFactor}); + }, + {}); + mountPendingTransaction(); +} + +void FabricMountingManagerTestHelper::overrideFeatureFlags( + jboolean fixTextLayoutOnPixelDensityChange) { + class TestFeatureFlags : public ReactNativeFeatureFlagsDefaults { + public: + explicit TestFeatureFlags(bool fixTextLayoutOnPixelDensityChange) + : fixTextLayoutOnPixelDensityChange_( + fixTextLayoutOnPixelDensityChange) {} + + bool fixTextLayoutOnPixelDensityChange() override { + return fixTextLayoutOnPixelDensityChange_; + } + + private: + bool fixTextLayoutOnPixelDensityChange_; + }; + ReactNativeFeatureFlags::dangerouslyForceOverride( + std::make_unique( + fixTextLayoutOnPixelDensityChange != 0)); +} + +void FabricMountingManagerTestHelper::resetFeatureFlags() { + ReactNativeFeatureFlags::dangerouslyReset(); +} + +void FabricMountingManagerTestHelper::mountPendingTransaction() { + auto transaction = shadowTree_->getMountingCoordinator()->pullTransaction(); + if (transaction.has_value()) { + mountingManager_->executeMount(*transaction); + } +} + void FabricMountingManagerTestHelper::registerNatives() { registerHybrid({ makeNativeMethod( @@ -99,6 +241,21 @@ void FabricMountingManagerTestHelper::registerNatives() { makeNativeMethod( "synchronouslyUpdateAnimatedProps", FabricMountingManagerTestHelper::synchronouslyUpdateAnimatedProps), + makeNativeMethod( + "startSurfaceWithConstraints", + FabricMountingManagerTestHelper::startSurfaceWithConstraints), + makeNativeMethod( + "mountViewWithPadding", + FabricMountingManagerTestHelper::mountViewWithPadding), + makeNativeMethod( + "updateSurfaceConstraints", + FabricMountingManagerTestHelper::updateSurfaceConstraints), + makeNativeMethod( + "overrideFeatureFlags", + FabricMountingManagerTestHelper::overrideFeatureFlags), + makeNativeMethod( + "resetFeatureFlags", + FabricMountingManagerTestHelper::resetFeatureFlags), }); } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test_helper/FabricMountingManagerTestHelper.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test_helper/FabricMountingManagerTestHelper.h index a1a05e36ab0..2e2b0192eca 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test_helper/FabricMountingManagerTestHelper.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/test_helper/FabricMountingManagerTestHelper.h @@ -10,6 +10,8 @@ #include #include #include +#include +#include #include @@ -30,6 +32,11 @@ class FabricMountingManagerTestHelper : public jni::HybridClass tags, jni::alias_ref> props); + void startSurfaceWithConstraints(jint surfaceId, jfloat widthDp, jfloat heightDp, jfloat pointScaleFactor); + void mountViewWithPadding(jint surfaceId, jint tag, jfloat padding); + void updateSurfaceConstraints(jint surfaceId, jfloat widthDp, jfloat heightDp, jfloat pointScaleFactor); + void overrideFeatureFlags(jboolean fixTextLayoutOnPixelDensityChange); + void resetFeatureFlags(); static void registerNatives(); @@ -42,9 +49,14 @@ class FabricMountingManagerTestHelper : public jni::HybridClass jobj, jni::alias_ref jFabricUIManager); + void mountPendingTransaction(); + jni::global_ref javaUIManager_; std::shared_ptr mountingManager_; + std::shared_ptr contextContainer_; std::unique_ptr viewComponentDescriptor_; + std::unique_ptr shadowTreeDelegate_; + std::unique_ptr shadowTree_; }; } // namespace facebook::react 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 9032b213d41..6bff0ece139 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<> + * @generated SignedSource<<200266835197634d13de07182d80a2c1>> */ /** @@ -399,6 +399,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool fixTextLayoutOnPixelDensityChange() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fixTextLayoutOnPixelDensityChange"); + return method(javaProvider_); + } + bool fixYogaFlexBasisFitContentInMainAxis() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("fixYogaFlexBasisFitContentInMainAxis"); @@ -895,6 +901,11 @@ bool JReactNativeFeatureFlagsCxxInterop::fixMappingOfEventPrioritiesBetweenFabri return ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact(); } +bool JReactNativeFeatureFlagsCxxInterop::fixTextLayoutOnPixelDensityChange( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange(); +} + bool JReactNativeFeatureFlagsCxxInterop::fixYogaFlexBasisFitContentInMainAxis( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::fixYogaFlexBasisFitContentInMainAxis(); @@ -1266,6 +1277,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "fixMappingOfEventPrioritiesBetweenFabricAndReact", JReactNativeFeatureFlagsCxxInterop::fixMappingOfEventPrioritiesBetweenFabricAndReact), + makeNativeMethod( + "fixTextLayoutOnPixelDensityChange", + JReactNativeFeatureFlagsCxxInterop::fixTextLayoutOnPixelDensityChange), makeNativeMethod( "fixYogaFlexBasisFitContentInMainAxis", JReactNativeFeatureFlagsCxxInterop::fixYogaFlexBasisFitContentInMainAxis), 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 295a5a92be9..e87b2422876 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<> + * @generated SignedSource<<13aa248dae556b06b31c25a28e448460>> */ /** @@ -210,6 +210,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool fixMappingOfEventPrioritiesBetweenFabricAndReact( facebook::jni::alias_ref); + static bool fixTextLayoutOnPixelDensityChange( + facebook::jni::alias_ref); + static bool fixYogaFlexBasisFitContentInMainAxis( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/FabricMountingManagerInstrumentationTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/FabricMountingManagerInstrumentationTest.kt index ef2040b9621..9f05e5f880b 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/FabricMountingManagerInstrumentationTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/FabricMountingManagerInstrumentationTest.kt @@ -7,6 +7,7 @@ package com.facebook.react.fabric +import android.graphics.Rect import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.facebook.react.ReactRootView @@ -26,6 +27,7 @@ import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewManager import com.facebook.react.uimanager.ViewManagerRegistry import com.facebook.react.uimanager.events.BatchEventDispatchedListener +import com.facebook.react.views.view.ReactViewGroup import com.facebook.react.views.view.ReactViewManager import com.facebook.soloader.SoLoader import org.assertj.core.api.Assertions.assertThat @@ -51,6 +53,7 @@ class FabricMountingManagerInstrumentationTest { private lateinit var mountingManager: MountingManager private lateinit var themedReactContext: ThemedReactContext private val surfaceId = 1 + private var nativeFeatureFlagsHelper: FabricMountingManagerTestHelper? = null companion object { @BeforeClass @@ -75,6 +78,7 @@ class FabricMountingManagerInstrumentationTest { @After fun tearDown() { + nativeFeatureFlagsHelper?.resetFeatureFlags() ReactNativeFeatureFlags.dangerouslyReset() } @@ -222,15 +226,39 @@ class FabricMountingManagerInstrumentationTest { // ---- Native code tests via FabricMountingManagerTestHelper ---- - private fun createFabricUIManager(): FabricUIManager { + private fun createFabricUIManager( + viewManagers: List> = listOf(ReactViewManager()), + ): FabricUIManager { val context = InstrumentationRegistry.getInstrumentation().targetContext val reactAppContext = mock(ReactApplicationContext::class.java) `when`(reactAppContext.applicationContext).thenReturn(context) `when`(reactAppContext.exceptionHandler).thenReturn(JSExceptionHandler {}) - val viewManagerRegistry = ViewManagerRegistry(listOf>(ReactViewManager())) + val viewManagerRegistry = ViewManagerRegistry(viewManagers) return FabricUIManager(reactAppContext, viewManagerRegistry, BatchEventDispatchedListener {}) } + private fun mountPaddedView( + fixTextLayoutOnPixelDensityChange: Boolean + ): Pair { + val fabricUIManager = createFabricUIManager(listOf(PaddingViewManager())) + val helper = FabricMountingManagerTestHelper.create(fabricUIManager) + helper.overrideFeatureFlags(fixTextLayoutOnPixelDensityChange) + nativeFeatureFlagsHelper = helper + fabricUIManager.setBinding(FabricUIManagerBinding()) + fabricUIManager.startSurface( + SurfaceHandlerBinding("TestSurface"), + themedReactContext, + ReactRootView(themedReactContext).apply { setRootViewTag(surfaceId) }, + ) + helper.startSurfaceWithConstraints(surfaceId, 300f, 300f, 2f) + lateinit var view: ReactViewGroup + InstrumentationRegistry.getInstrumentation().runOnMainSync { + helper.mountViewWithPadding(surfaceId, 42, 10f) + view = fabricUIManager.resolveView(42) as ReactViewGroup + } + return helper to view + } + private fun createTestHelper(): FabricMountingManagerTestHelper = FabricMountingManagerTestHelper.create(createFabricUIManager()) @@ -335,4 +363,37 @@ class FabricMountingManagerInstrumentationTest { assertThat(fallbackView.alpha).isEqualTo(0.5f) assertThat(fallbackView.tag).isEqualTo("animated-view") } + + @Test + fun native_updateSurfaceConstraints_resendsPaddingAndOverflowInsetInPixels() { + val (helper, view) = mountPaddedView(fixTextLayoutOnPixelDensityChange = true) + assertThat(view.paddingTop).isEqualTo(20) + assertThat(view.overflowInset).isEqualTo(Rect(-20, -20, -20, -20)) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + helper.updateSurfaceConstraints(surfaceId, 300f, 300f, 1f) + } + + assertThat(intArrayOf(view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom)) + .containsExactly(10, 10, 10, 10) + assertThat(view.overflowInset).isEqualTo(Rect(-10, -10, -10, -10)) + } + + @Test + fun native_updateSurfaceConstraints_withoutFlag_keepsPaddingAndOverflowInsetInPixels() { + val (helper, view) = mountPaddedView(fixTextLayoutOnPixelDensityChange = false) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + helper.updateSurfaceConstraints(surfaceId, 300f, 300f, 1f) + } + + assertThat(view.paddingTop).isEqualTo(20) + assertThat(view.overflowInset).isEqualTo(Rect(-20, -20, -20, -20)) + } + + private class PaddingViewManager : ReactViewManager() { + override fun setPadding(view: ReactViewGroup, left: Int, top: Int, right: Int, bottom: Int) { + view.setPadding(left, top, right, bottom) + } + } } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/FabricMountingManagerTestHelper.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/FabricMountingManagerTestHelper.kt index bba2ec0408b..ce133c1d0a6 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/FabricMountingManagerTestHelper.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/FabricMountingManagerTestHelper.kt @@ -37,6 +37,26 @@ class FabricMountingManagerTestHelper private constructor(fabricUIManager: Fabri external fun synchronouslyUpdateAnimatedProps(tags: IntArray, props: Array) + external fun startSurfaceWithConstraints( + surfaceId: Int, + widthDp: Float, + heightDp: Float, + pointScaleFactor: Float, + ) + + external fun mountViewWithPadding(surfaceId: Int, tag: Int, padding: Float) + + external fun updateSurfaceConstraints( + surfaceId: Int, + widthDp: Float, + heightDp: Float, + pointScaleFactor: Float, + ) + + external fun overrideFeatureFlags(fixTextLayoutOnPixelDensityChange: Boolean) + + external fun resetFeatureFlags() + companion object { init { SoLoader.loadLibrary("fabricjni") diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index f0ec50d7ac0..1744ea85445 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<<62fecb7f375888d2669b09cfb853d0f9>> + * @generated SignedSource<> */ /** @@ -266,6 +266,10 @@ bool ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact() return getAccessor().fixMappingOfEventPrioritiesBetweenFabricAndReact(); } +bool ReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange() { + return getAccessor().fixTextLayoutOnPixelDensityChange(); +} + bool ReactNativeFeatureFlags::fixYogaFlexBasisFitContentInMainAxis() { return getAccessor().fixYogaFlexBasisFitContentInMainAxis(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index e945ec15cb0..c5195f19bd5 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<<2bace47aeee75bd0e4d2602403a67490>> + * @generated SignedSource<<1a9a8a587b477b6aa0d3aa57e30272c2>> */ /** @@ -341,6 +341,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool fixMappingOfEventPrioritiesBetweenFabricAndReact(); + /** + * Re-measures text and re-sends padding and overflow insets to mounted views when the pixel density changes, so text and padded views do not keep sizes from the old density. + */ + RN_EXPORT static bool fixTextLayoutOnPixelDensityChange(); + /** * Fix flex basis computation to not apply FitContent constraint in the main axis for non-measure container nodes, preventing unnecessary re-measurement in scroll containers. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index f5de8b70c67..3d332f2976f 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<> + * @generated SignedSource<> */ /** @@ -1109,6 +1109,24 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::fixTextLayoutOnPixelDensityChange() { + auto flagValue = fixTextLayoutOnPixelDensityChange_.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(60, "fixTextLayoutOnPixelDensityChange"); + + flagValue = currentProvider_->fixTextLayoutOnPixelDensityChange(); + fixTextLayoutOnPixelDensityChange_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::fixYogaFlexBasisFitContentInMainAxis() { auto flagValue = fixYogaFlexBasisFitContentInMainAxis_.load(); @@ -1118,7 +1136,7 @@ bool ReactNativeFeatureFlagsAccessor::fixYogaFlexBasisFitContentInMainAxis() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(60, "fixYogaFlexBasisFitContentInMainAxis"); + markFlagAsAccessed(61, "fixYogaFlexBasisFitContentInMainAxis"); flagValue = currentProvider_->fixYogaFlexBasisFitContentInMainAxis(); fixYogaFlexBasisFitContentInMainAxis_ = flagValue; @@ -1136,7 +1154,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxAssertSingleHostState() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(61, "fuseboxAssertSingleHostState"); + markFlagAsAccessed(62, "fuseboxAssertSingleHostState"); flagValue = currentProvider_->fuseboxAssertSingleHostState(); fuseboxAssertSingleHostState_ = flagValue; @@ -1154,7 +1172,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(62, "fuseboxEnabledRelease"); + markFlagAsAccessed(63, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -1172,7 +1190,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxFrameRecordingEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(63, "fuseboxFrameRecordingEnabled"); + markFlagAsAccessed(64, "fuseboxFrameRecordingEnabled"); flagValue = currentProvider_->fuseboxFrameRecordingEnabled(); fuseboxFrameRecordingEnabled_ = flagValue; @@ -1190,7 +1208,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxScreenshotCaptureEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "fuseboxScreenshotCaptureEnabled"); + markFlagAsAccessed(65, "fuseboxScreenshotCaptureEnabled"); flagValue = currentProvider_->fuseboxScreenshotCaptureEnabled(); fuseboxScreenshotCaptureEnabled_ = flagValue; @@ -1208,7 +1226,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxWebSocketEventsEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(65, "fuseboxWebSocketEventsEnabled"); + markFlagAsAccessed(66, "fuseboxWebSocketEventsEnabled"); flagValue = currentProvider_->fuseboxWebSocketEventsEnabled(); fuseboxWebSocketEventsEnabled_ = flagValue; @@ -1226,7 +1244,7 @@ bool ReactNativeFeatureFlagsAccessor::optimizedAnimatedPropUpdates() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(66, "optimizedAnimatedPropUpdates"); + markFlagAsAccessed(67, "optimizedAnimatedPropUpdates"); flagValue = currentProvider_->optimizedAnimatedPropUpdates(); optimizedAnimatedPropUpdates_ = flagValue; @@ -1244,7 +1262,7 @@ bool ReactNativeFeatureFlagsAccessor::overrideBySynchronousMountPropsAtMountingA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(67, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(68, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1262,7 +1280,7 @@ bool ReactNativeFeatureFlagsAccessor::perfIssuesEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(68, "perfIssuesEnabled"); + markFlagAsAccessed(69, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1280,7 +1298,7 @@ bool ReactNativeFeatureFlagsAccessor::perfMonitorV2Enabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "perfMonitorV2Enabled"); + markFlagAsAccessed(70, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1298,7 +1316,7 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "preparedTextCacheSize"); + markFlagAsAccessed(71, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1316,7 +1334,7 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(71, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(72, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1334,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2Android() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "redBoxV2Android"); + markFlagAsAccessed(73, "redBoxV2Android"); flagValue = currentProvider_->redBoxV2Android(); redBoxV2Android_ = flagValue; @@ -1352,7 +1370,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2IOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(73, "redBoxV2IOS"); + markFlagAsAccessed(74, "redBoxV2IOS"); flagValue = currentProvider_->redBoxV2IOS(); redBoxV2IOS_ = flagValue; @@ -1370,7 +1388,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(74, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(75, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1388,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(76, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1406,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(77, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1424,7 +1442,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(77, "syncAndroidClipBoundsWithOverflow"); + markFlagAsAccessed(78, "syncAndroidClipBoundsWithOverflow"); flagValue = currentProvider_->syncAndroidClipBoundsWithOverflow(); syncAndroidClipBoundsWithOverflow_ = flagValue; @@ -1442,7 +1460,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(78, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(79, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1460,7 +1478,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(79, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(80, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1478,7 +1496,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(80, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(81, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1496,7 +1514,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(81, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(82, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1514,7 +1532,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(82, "useFabricInterop"); + markFlagAsAccessed(83, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1532,7 +1550,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(83, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(84, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1550,7 +1568,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(84, "useNestedScrollViewAndroid"); + markFlagAsAccessed(85, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1568,7 +1586,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(85, "useSharedAnimatedBackend"); + markFlagAsAccessed(86, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1586,7 +1604,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(86, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(87, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1604,7 +1622,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(87, "useTurboModuleInterop"); + markFlagAsAccessed(88, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1622,7 +1640,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(88, "viewCullingOutsetRatio"); + markFlagAsAccessed(89, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1640,7 +1658,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(89, "viewTransitionEnabled"); + markFlagAsAccessed(90, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1658,7 +1676,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(90, "viewTransitionUseHardwareBitmapAndroid"); + markFlagAsAccessed(91, "viewTransitionUseHardwareBitmapAndroid"); flagValue = currentProvider_->viewTransitionUseHardwareBitmapAndroid(); viewTransitionUseHardwareBitmapAndroid_ = flagValue; @@ -1676,7 +1694,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(91, "virtualViewPrerenderRatio"); + markFlagAsAccessed(92, "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 b51f1227a74..fc7efe0b5c7 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<<583c65303e8d980070447b2833e33d89>> + * @generated SignedSource<<486b4aac1c83d8bced6a3f5393a03756>> */ /** @@ -94,6 +94,7 @@ class ReactNativeFeatureFlagsAccessor { bool enableVirtualViewContainerStateExperimental(); bool fixBorderlessRippleAndroid(); bool fixMappingOfEventPrioritiesBetweenFabricAndReact(); + bool fixTextLayoutOnPixelDensityChange(); bool fixYogaFlexBasisFitContentInMainAxis(); bool fuseboxAssertSingleHostState(); bool fuseboxEnabledRelease(); @@ -137,7 +138,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 92> accessedFeatureFlags_; + std::array, 93> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -199,6 +200,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableVirtualViewContainerStateExperimental_; std::atomic> fixBorderlessRippleAndroid_; std::atomic> fixMappingOfEventPrioritiesBetweenFabricAndReact_; + std::atomic> fixTextLayoutOnPixelDensityChange_; std::atomic> fixYogaFlexBasisFitContentInMainAxis_; std::atomic> fuseboxAssertSingleHostState_; std::atomic> fuseboxEnabledRelease_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 7e398cdffda..92e1157fefb 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<<8a82a13c242d68512b2e87e723f93aa3>> + * @generated SignedSource<> */ /** @@ -269,6 +269,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool fixTextLayoutOnPixelDensityChange() override { + return false; + } + bool fixYogaFlexBasisFitContentInMainAxis() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 8cf4e44470b..a49d8e7e919 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<<9272123d1a9171ac730313ebbd0c74f2>> + * @generated SignedSource<<2a5ad585617d77035a43b255d3e30eb3>> */ /** @@ -587,6 +587,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::fixMappingOfEventPrioritiesBetweenFabricAndReact(); } + bool fixTextLayoutOnPixelDensityChange() override { + auto value = values_["fixTextLayoutOnPixelDensityChange"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::fixTextLayoutOnPixelDensityChange(); + } + bool fixYogaFlexBasisFitContentInMainAxis() override { auto value = values_["fixYogaFlexBasisFitContentInMainAxis"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 0138142f05d..eed6e5e8a51 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<<0b840d8580f1d078fa1b7781b82c3281>> + * @generated SignedSource<<8daa3cebd554caa27139d96604fcc368>> */ /** @@ -87,6 +87,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableVirtualViewContainerStateExperimental() = 0; virtual bool fixBorderlessRippleAndroid() = 0; virtual bool fixMappingOfEventPrioritiesBetweenFabricAndReact() = 0; + virtual bool fixTextLayoutOnPixelDensityChange() = 0; virtual bool fixYogaFlexBasisFitContentInMainAxis() = 0; virtual bool fuseboxAssertSingleHostState() = 0; virtual bool fuseboxEnabledRelease() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index f51f19fe080..55a535f5be9 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<<00cccf7098052c51f5855949375ea0f9>> */ /** @@ -344,6 +344,11 @@ bool NativeReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndR return ReactNativeFeatureFlags::fixMappingOfEventPrioritiesBetweenFabricAndReact(); } +bool NativeReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange(); +} + bool NativeReactNativeFeatureFlags::fixYogaFlexBasisFitContentInMainAxis( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::fixYogaFlexBasisFitContentInMainAxis(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 125e7890dc0..459619f3427 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<> */ /** @@ -158,6 +158,8 @@ class NativeReactNativeFeatureFlags bool fixMappingOfEventPrioritiesBetweenFabricAndReact(jsi::Runtime& runtime); + bool fixTextLayoutOnPixelDensityChange(jsi::Runtime& runtime); + bool fixYogaFlexBasisFitContentInMainAxis(jsi::Runtime& runtime); bool fuseboxAssertSingleHostState(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp index 45edd26670b..7602b616e79 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp @@ -153,7 +153,12 @@ void ParagraphShadowNode::updateStateIfNeeded( react_native_assert(textLayoutManager_); - if (state.measuredLayout.measurement.size == layout.measurement.size && + bool pointScaleFactorChanged = + ReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange() && + state.measuredLayout.pointScaleFactor != layout.pointScaleFactor; + + if (!pointScaleFactorChanged && + state.measuredLayout.measurement.size == layout.measurement.size && state.attributedString == content.attributedString && state.paragraphAttributes == content.paragraphAttributes) { return; @@ -255,6 +260,7 @@ Size ParagraphShadowNode::measureContent( measuredLayouts_.push_back( MeasuredPreparedTextLayout{ .layoutConstraints = layoutConstraints, + .pointScaleFactor = layoutContext.pointScaleFactor, .measurement = measurement, // PreparedTextLayout is not trivially copyable on all platforms // NOLINTNEXTLINE(performance-move-const-arg) @@ -337,6 +343,10 @@ void ParagraphShadowNode::layout(LayoutContext layoutContext) { react_native_assert(measuredLayout); updateStateIfNeeded(content, *measuredLayout); + } else if (ReactNativeFeatureFlags::fixTextLayoutOnPixelDensityChange()) { + MeasuredPreparedTextLayout unpreparedLayout; + unpreparedLayout.pointScaleFactor = layoutContext.pointScaleFactor; + updateStateIfNeeded(content, unpreparedLayout); } else { updateStateIfNeeded(content); } diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextLayoutManagerExtended.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextLayoutManagerExtended.h index a0aaf283b59..93677133e64 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextLayoutManagerExtended.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextLayoutManagerExtended.h @@ -118,6 +118,7 @@ using TextLayoutManagerExtended = detail::TextLayoutManagerExtended> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -110,6 +110,7 @@ export type ReactNativeFeatureFlags = Readonly<{ enableVirtualViewContainerStateExperimental: Getter, fixBorderlessRippleAndroid: Getter, fixMappingOfEventPrioritiesBetweenFabricAndReact: Getter, + fixTextLayoutOnPixelDensityChange: Getter, fixYogaFlexBasisFitContentInMainAxis: Getter, fuseboxAssertSingleHostState: Getter, fuseboxEnabledRelease: Getter, @@ -463,6 +464,10 @@ export const fixBorderlessRippleAndroid: Getter = createNativeFlagGette * Uses the default event priority instead of the discreet event priority by default when dispatching events from Fabric to React. */ export const fixMappingOfEventPrioritiesBetweenFabricAndReact: Getter = createNativeFlagGetter('fixMappingOfEventPrioritiesBetweenFabricAndReact', false); +/** + * Re-measures text and re-sends padding and overflow insets to mounted views when the pixel density changes, so text and padded views do not keep sizes from the old density. + */ +export const fixTextLayoutOnPixelDensityChange: Getter = createNativeFlagGetter('fixTextLayoutOnPixelDensityChange', false); /** * Fix flex basis computation to not apply FitContent constraint in the main axis for non-measure container nodes, preventing unnecessary re-measurement in scroll containers. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 47adcd1fbc1..b73154a7485 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<<3972c873d92371d3dc67eaa3a6217427>> * @flow strict * @noformat */ @@ -85,6 +85,7 @@ export interface Spec extends TurboModule { readonly enableVirtualViewContainerStateExperimental?: () => boolean; readonly fixBorderlessRippleAndroid?: () => boolean; readonly fixMappingOfEventPrioritiesBetweenFabricAndReact?: () => boolean; + readonly fixTextLayoutOnPixelDensityChange?: () => boolean; readonly fixYogaFlexBasisFitContentInMainAxis?: () => boolean; readonly fuseboxAssertSingleHostState?: () => boolean; readonly fuseboxEnabledRelease?: () => boolean; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index a1f38046aa9..b4025cd57ae 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -6251,6 +6251,7 @@ struct facebook::react::Magic32 { } struct facebook::react::MeasuredPreparedTextLayout { + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::TextMeasurement measurement; public facebook::react::detail::TextLayoutManagerExtended::PreparedTextLayout preparedTextLayout; diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index c9948285d78..9225f914db8 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -8338,6 +8338,7 @@ struct facebook::react::Magic32 { } struct facebook::react::MeasuredPreparedTextLayout { + public facebook::react::Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::TextMeasurement measurement; public facebook::react::detail::TextLayoutManagerExtended::PreparedTextLayout preparedTextLayout; diff --git a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api index 476546b2050..eff6ee8a279 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api @@ -4488,6 +4488,7 @@ struct facebook::react::Magic32 { } struct facebook::react::MeasuredPreparedTextLayout { + public Float pointScaleFactor; public facebook::react::LayoutConstraints layoutConstraints; public facebook::react::TextMeasurement measurement; public facebook::react::detail::TextLayoutManagerExtended::PreparedTextLayout preparedTextLayout;