From 30dac4777d06e4b79a6e346c895c932354bf5240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 25 Mar 2024 05:06:27 -0700 Subject: [PATCH 1/3] Implement UI consistency in JS thread Differential Revision: D55024832 --- .../React/React-RCTFabric.podspec | 1 + .../ReactAndroid/build.gradle.kts | 2 + .../featureflags/ReactNativeFeatureFlags.kt | 8 ++- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 +++- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 +++- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../ReactAndroid/src/main/jni/CMakeLists.txt | 2 + .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 ++++- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../ReactCommon/React-Fabric.podspec | 13 +++- .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 26 +++++-- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../react/renderer/consistency/CMakeLists.txt | 20 ++++++ .../React-rendererconsistency.podspec | 45 ++++++++++++ .../ScopedShadowTreeRevisionLock.h | 54 ++++++++++++++ .../ShadowTreeRevisionConsistencyManager.cpp | 14 ++++ .../ShadowTreeRevisionConsistencyManager.h | 24 +++++++ .../renderer/runtimescheduler/CMakeLists.txt | 1 + .../React-runtimescheduler.podspec | 1 + .../runtimescheduler/RuntimeScheduler.cpp | 7 ++ .../runtimescheduler/RuntimeScheduler.h | 7 ++ .../RuntimeScheduler_Legacy.cpp | 37 +++++++--- .../RuntimeScheduler_Legacy.h | 8 +++ .../RuntimeScheduler_Modern.cpp | 28 +++++--- .../RuntimeScheduler_Modern.h | 7 ++ .../react/renderer/scheduler/CMakeLists.txt | 1 + .../react/renderer/scheduler/Scheduler.cpp | 6 ++ .../react/renderer/uimanager/CMakeLists.txt | 2 + .../react/renderer/uimanager/UIManager.cpp | 61 ++++++++++++---- .../react/renderer/uimanager/UIManager.h | 15 +++- .../uimanager/consistency/CMakeLists.txt | 26 +++++++ .../LatestShadowTreeRevisionProvider.cpp | 29 ++++++++ .../LatestShadowTreeRevisionProvider.h | 35 +++++++++ ...zyShadowTreeRevisionConsistencyManager.cpp | 72 +++++++++++++++++++ ...LazyShadowTreeRevisionConsistencyManager.h | 51 +++++++++++++ .../consistency/ShadowTreeRevisionProvider.h | 27 +++++++ .../ReactNativeFeatureFlags.config.js | 5 ++ .../react-native/scripts/react_native_pods.rb | 1 + .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- 48 files changed, 689 insertions(+), 54 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/consistency/CMakeLists.txt create mode 100644 packages/react-native/ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec create mode 100644 packages/react-native/ReactCommon/react/renderer/consistency/ScopedShadowTreeRevisionLock.h create mode 100644 packages/react-native/ReactCommon/react/renderer/consistency/ShadowTreeRevisionConsistencyManager.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/consistency/ShadowTreeRevisionConsistencyManager.h create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/consistency/CMakeLists.txt create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LatestShadowTreeRevisionProvider.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LatestShadowTreeRevisionProvider.h create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.h create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/consistency/ShadowTreeRevisionProvider.h diff --git a/packages/react-native/React/React-RCTFabric.podspec b/packages/react-native/React/React-RCTFabric.podspec index 9a28eeec44dc..7cfb18a8dd1d 100644 --- a/packages/react-native/React/React-RCTFabric.podspec +++ b/packages/react-native/React/React-RCTFabric.podspec @@ -87,6 +87,7 @@ Pod::Spec.new do |s| add_dependency(s, "React-debug") add_dependency(s, "React-utils") add_dependency(s, "React-rendererdebug") + add_dependency(s, "React-rendererconsistency") add_dependency(s, "React-runtimescheduler") add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern') diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 711181509ede..3a73ee13681b 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -563,6 +563,7 @@ android { "react_cxxreactpackage", "react_render_animations", "react_render_core", + "react_render_consistency", "react_render_graphics", "rrc_image", "rrc_root", @@ -580,6 +581,7 @@ android { "react_nativemodule_core", "react_render_imagemanager", "react_render_uimanager", + "react_render_uimanager_consistency", "react_render_scheduler", "react_render_mounting", "hermes_executor", 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 a14811239d8f..50eabadaab68 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<<20c445fde7a1c2607b58b7797346431f>> */ /** @@ -76,6 +76,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun enableSpannableBuildingUnification(): Boolean = accessor.enableSpannableBuildingUnification() + /** + * Ensures that JavaScript always has a consistent view of the state of the UI (e.g.: commits done in other threads are not immediately propagated to JS during its execution). + */ + @JvmStatic + public fun enableUIConsistency(): Boolean = accessor.enableUIConsistency() + /** * Flag determining if the C++ implementation of InspectorPackagerConnection should be used instead of the per-platform one. This flag is global and should not be changed across React Host lifetimes. */ 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 e65fa7974529..7df37086a8a0 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<<7ae379135157666d9646f1d8eeec9989>> + * @generated SignedSource<<6e973dcdcdfae14c77a6130207333551>> */ /** @@ -28,6 +28,7 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso private var enableMicrotasksCache: Boolean? = null private var enableMountHooksAndroidCache: Boolean? = null private var enableSpannableBuildingUnificationCache: Boolean? = null + private var enableUIConsistencyCache: Boolean? = null private var inspectorEnableCxxInspectorPackagerConnectionCache: Boolean? = null private var inspectorEnableModernCDPRegistryCache: Boolean? = null private var useModernRuntimeSchedulerCache: Boolean? = null @@ -104,6 +105,15 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso return cached } + override fun enableUIConsistency(): Boolean { + var cached = enableUIConsistencyCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.enableUIConsistency() + enableUIConsistencyCache = cached + } + return cached + } + override fun inspectorEnableCxxInspectorPackagerConnection(): Boolean { var cached = inspectorEnableCxxInspectorPackagerConnectionCache 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 d6093108c73d..465c71cf32c9 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<<244a0656beee8e018585bdd4bb4e5cd1>> + * @generated SignedSource<<0d34567fc05555ae401e7f5180dc8771>> */ /** @@ -44,6 +44,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun enableSpannableBuildingUnification(): Boolean + @DoNotStrip @JvmStatic public external fun enableUIConsistency(): Boolean + @DoNotStrip @JvmStatic public external fun inspectorEnableCxxInspectorPackagerConnection(): Boolean @DoNotStrip @JvmStatic public external fun inspectorEnableModernCDPRegistry(): 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 7a2ac9d16016..1e0f7a392d8a 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<> */ /** @@ -39,6 +39,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun enableSpannableBuildingUnification(): Boolean = false + override fun enableUIConsistency(): Boolean = false + override fun inspectorEnableCxxInspectorPackagerConnection(): Boolean = false override fun inspectorEnableModernCDPRegistry(): 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 cae01383bef9..698251ab09d0 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<<919eb0f27540e5dd7a1e028663c23264>> + * @generated SignedSource<<8908de9b9d0186f1916d8b55d5854cb2>> */ /** @@ -32,6 +32,7 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces private var enableMicrotasksCache: Boolean? = null private var enableMountHooksAndroidCache: Boolean? = null private var enableSpannableBuildingUnificationCache: Boolean? = null + private var enableUIConsistencyCache: Boolean? = null private var inspectorEnableCxxInspectorPackagerConnectionCache: Boolean? = null private var inspectorEnableModernCDPRegistryCache: Boolean? = null private var useModernRuntimeSchedulerCache: Boolean? = null @@ -116,6 +117,16 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun enableUIConsistency(): Boolean { + var cached = enableUIConsistencyCache + if (cached == null) { + cached = currentProvider.enableUIConsistency() + accessedFeatureFlags.add("enableUIConsistency") + enableUIConsistencyCache = cached + } + return cached + } + override fun inspectorEnableCxxInspectorPackagerConnection(): Boolean { var cached = inspectorEnableCxxInspectorPackagerConnectionCache 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 451f64be6d98..1bd4b3ad6a42 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<<7555a704535615fcea44c1261095419a>> */ /** @@ -39,6 +39,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun enableSpannableBuildingUnification(): Boolean + @DoNotStrip public fun enableUIConsistency(): Boolean + @DoNotStrip public fun inspectorEnableCxxInspectorPackagerConnection(): Boolean @DoNotStrip public fun inspectorEnableModernCDPRegistry(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt index af48dbd60eee..5c0d6a9cd7fa 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt @@ -79,6 +79,8 @@ add_react_common_subdir(react/renderer/scheduler) add_react_common_subdir(react/renderer/telemetry) add_react_common_subdir(react/renderer/uimanager) add_react_common_subdir(react/renderer/core) +add_react_common_subdir(react/renderer/consistency) +add_react_common_subdir(react/renderer/uimanager/consistency) add_react_common_subdir(react/renderer/element) add_react_common_subdir(react/renderer/graphics) add_react_common_subdir(react/renderer/debug) 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 4add6436bbd4..d24467eb2056 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<<393c6cf93399cfe0b0533927877531d5>> + * @generated SignedSource<> */ /** @@ -87,6 +87,12 @@ class ReactNativeFeatureFlagsProviderHolder return method(javaProvider_); } + bool enableUIConsistency() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableUIConsistency"); + return method(javaProvider_); + } + bool inspectorEnableCxxInspectorPackagerConnection() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("inspectorEnableCxxInspectorPackagerConnection"); @@ -149,6 +155,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableSpannableBuildingUnification( return ReactNativeFeatureFlags::enableSpannableBuildingUnification(); } +bool JReactNativeFeatureFlagsCxxInterop::enableUIConsistency( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::enableUIConsistency(); +} + bool JReactNativeFeatureFlagsCxxInterop::inspectorEnableCxxInspectorPackagerConnection( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::inspectorEnableCxxInspectorPackagerConnection(); @@ -205,6 +216,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "enableSpannableBuildingUnification", JReactNativeFeatureFlagsCxxInterop::enableSpannableBuildingUnification), + makeNativeMethod( + "enableUIConsistency", + JReactNativeFeatureFlagsCxxInterop::enableUIConsistency), makeNativeMethod( "inspectorEnableCxxInspectorPackagerConnection", JReactNativeFeatureFlagsCxxInterop::inspectorEnableCxxInspectorPackagerConnection), 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 d710f2c40522..27e41e73a4e4 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<> */ /** @@ -54,6 +54,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool enableSpannableBuildingUnification( facebook::jni::alias_ref); + static bool enableUIConsistency( + facebook::jni::alias_ref); + static bool inspectorEnableCxxInspectorPackagerConnection( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index 6662a301291e..05d0e7259e59 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -213,7 +213,7 @@ Pod::Spec.new do |s| sss.header_dir = "react/renderer/components/textinput" end - + ss.subspec "unimplementedview" do |sss| sss.dependency folly_dep_name, folly_version sss.compiler_flags = folly_compiler_flags @@ -277,10 +277,17 @@ Pod::Spec.new do |s| end s.subspec "uimanager" do |ss| + ss.subspec "consistency" do |sss| + sss.dependency folly_dep_name, folly_version + sss.compiler_flags = folly_compiler_flags + sss.source_files = "react/renderer/uimanager/consistency/*.{m,mm,cpp,h}" + sss.header_dir = "react/renderer/uimanager/consistency" + end + ss.dependency folly_dep_name, folly_version + ss.dependency "React-rendererconsistency" ss.compiler_flags = folly_compiler_flags - ss.source_files = "react/renderer/uimanager/**/*.{m,mm,cpp,h}" - ss.exclude_files = "react/renderer/uimanager/tests" + ss.source_files = "react/renderer/uimanager/*.{m,mm,cpp,h}" ss.header_dir = "react/renderer/uimanager" end diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index 163c9e238845..2a4c352c5a4e 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<> + * @generated SignedSource<> */ /** @@ -53,6 +53,10 @@ bool ReactNativeFeatureFlags::enableSpannableBuildingUnification() { return getAccessor().enableSpannableBuildingUnification(); } +bool ReactNativeFeatureFlags::enableUIConsistency() { + return getAccessor().enableUIConsistency(); +} + bool ReactNativeFeatureFlags::inspectorEnableCxxInspectorPackagerConnection() { return getAccessor().inspectorEnableCxxInspectorPackagerConnection(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 9f51ae019bc4..0354f83b5473 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<> + * @generated SignedSource<> */ /** @@ -77,6 +77,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool enableSpannableBuildingUnification(); + /** + * Ensures that JavaScript always has a consistent view of the state of the UI (e.g.: commits done in other threads are not immediately propagated to JS during its execution). + */ + RN_EXPORT static bool enableUIConsistency(); + /** * Flag determining if the C++ implementation of InspectorPackagerConnection should be used instead of the per-platform one. This flag is global and should not be changed across React Host lifetimes. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 9adeb8f60d42..f7a4feb4dbbd 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<<2ff39fd4c8330ddca994fc40cdeaaf4c>> */ /** @@ -173,6 +173,24 @@ bool ReactNativeFeatureFlagsAccessor::enableSpannableBuildingUnification() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::enableUIConsistency() { + auto flagValue = enableUIConsistency_.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(8, "enableUIConsistency"); + + flagValue = currentProvider_->enableUIConsistency(); + enableUIConsistency_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::inspectorEnableCxxInspectorPackagerConnection() { auto flagValue = inspectorEnableCxxInspectorPackagerConnection_.load(); @@ -182,7 +200,7 @@ bool ReactNativeFeatureFlagsAccessor::inspectorEnableCxxInspectorPackagerConnect // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(8, "inspectorEnableCxxInspectorPackagerConnection"); + markFlagAsAccessed(9, "inspectorEnableCxxInspectorPackagerConnection"); flagValue = currentProvider_->inspectorEnableCxxInspectorPackagerConnection(); inspectorEnableCxxInspectorPackagerConnection_ = flagValue; @@ -200,7 +218,7 @@ bool ReactNativeFeatureFlagsAccessor::inspectorEnableModernCDPRegistry() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(9, "inspectorEnableModernCDPRegistry"); + markFlagAsAccessed(10, "inspectorEnableModernCDPRegistry"); flagValue = currentProvider_->inspectorEnableModernCDPRegistry(); inspectorEnableModernCDPRegistry_ = flagValue; @@ -218,7 +236,7 @@ bool ReactNativeFeatureFlagsAccessor::useModernRuntimeScheduler() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(10, "useModernRuntimeScheduler"); + markFlagAsAccessed(11, "useModernRuntimeScheduler"); flagValue = currentProvider_->useModernRuntimeScheduler(); useModernRuntimeScheduler_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 5199fc1ca593..dc3b1f0b99cb 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<> + * @generated SignedSource<> */ /** @@ -39,6 +39,7 @@ class ReactNativeFeatureFlagsAccessor { bool enableMicrotasks(); bool enableMountHooksAndroid(); bool enableSpannableBuildingUnification(); + bool enableUIConsistency(); bool inspectorEnableCxxInspectorPackagerConnection(); bool inspectorEnableModernCDPRegistry(); bool useModernRuntimeScheduler(); @@ -52,7 +53,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 11> accessedFeatureFlags_; + std::array, 12> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> batchRenderingUpdatesInEventLoop_; @@ -62,6 +63,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enableMicrotasks_; std::atomic> enableMountHooksAndroid_; std::atomic> enableSpannableBuildingUnification_; + std::atomic> enableUIConsistency_; std::atomic> inspectorEnableCxxInspectorPackagerConnection_; std::atomic> inspectorEnableModernCDPRegistry_; std::atomic> useModernRuntimeScheduler_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 85f6d322e89d..0115a835c636 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<<62de1b0e27590ad769296358a4f42c7a>> + * @generated SignedSource<<1450d89abc68821fb348574016874719>> */ /** @@ -59,6 +59,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool enableUIConsistency() override { + return false; + } + bool inspectorEnableCxxInspectorPackagerConnection() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 48ec9f28697f..cbf80cf641e0 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<<3117fc0389416297369a47ee480eb906>> + * @generated SignedSource<> */ /** @@ -33,6 +33,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enableMicrotasks() = 0; virtual bool enableMountHooksAndroid() = 0; virtual bool enableSpannableBuildingUnification() = 0; + virtual bool enableUIConsistency() = 0; virtual bool inspectorEnableCxxInspectorPackagerConnection() = 0; virtual bool inspectorEnableModernCDPRegistry() = 0; virtual bool useModernRuntimeScheduler() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index 30c6d98f483d..8a957343c12f 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<<27923a2dbf1dcbad238a4d06ebb54fb5>> + * @generated SignedSource<<7e09a7ad1d178850bdcf73da3eb5623b>> */ /** @@ -77,6 +77,11 @@ bool NativeReactNativeFeatureFlags::enableSpannableBuildingUnification( return ReactNativeFeatureFlags::enableSpannableBuildingUnification(); } +bool NativeReactNativeFeatureFlags::enableUIConsistency( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::enableUIConsistency(); +} + bool NativeReactNativeFeatureFlags::inspectorEnableCxxInspectorPackagerConnection( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::inspectorEnableCxxInspectorPackagerConnection(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index ffe0f7044d54..bdc52a818d5c 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<<53251a11631eccf1fcc334b14f9ca4c6>> + * @generated SignedSource<> */ /** @@ -51,6 +51,8 @@ class NativeReactNativeFeatureFlags bool enableSpannableBuildingUnification(jsi::Runtime& runtime); + bool enableUIConsistency(jsi::Runtime& runtime); + bool inspectorEnableCxxInspectorPackagerConnection(jsi::Runtime& runtime); bool inspectorEnableModernCDPRegistry(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/renderer/consistency/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/consistency/CMakeLists.txt new file mode 100644 index 000000000000..4273bb456145 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/consistency/CMakeLists.txt @@ -0,0 +1,20 @@ +# 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. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +add_compile_options( + -fexceptions + -frtti + -std=c++20 + -Wall + -Wpedantic + -DLOG_TAG=\"Fabric\") + +file(GLOB react_render_consistency_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_render_consistency SHARED ${react_render_consistency_SRC}) + +target_include_directories(react_render_consistency PUBLIC ${REACT_COMMON_DIR}) diff --git a/packages/react-native/ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec b/packages/react-native/ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec new file mode 100644 index 000000000000..873eb7c96818 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/consistency/React-rendererconsistency.podspec @@ -0,0 +1,45 @@ +# 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. + +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "..", "..", "..", "..", "package.json"))) +version = package['version'] + +source = { :git => 'https://github.com/facebook/react-native.git' } +if version == '1000.0.0' + # This is an unpublished version, use the latest commit hash of the react-native repo, which we’re presumably in. + source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1") +else + source[:tag] = "v#{version}" +end + +header_search_paths = [] + +if ENV['USE_FRAMEWORKS'] + header_search_paths << "\"$(PODS_TARGET_SRCROOT)/../../..\"" # this is needed to allow the rendererconsistency access its own files +end + +Pod::Spec.new do |s| + s.name = "React-rendererconsistency" + s.version = version + s.summary = "Fabric UI consistency primitives" + s.homepage = "https://reactnative.dev/" + s.license = package["license"] + s.author = "Meta Platforms, Inc. and its affiliates" + s.platforms = min_supported_versions + s.source = source + s.source_files = "**/*.{cpp,h}" + s.header_dir = "react/renderer/consistency" + s.exclude_files = "tests" + s.pod_target_xcconfig = { + "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + "HEADER_SEARCH_PATHS" => header_search_paths.join(' ')} + + if ENV['USE_FRAMEWORKS'] + s.module_name = "React_rendererconsistency" + s.header_mappings_dir = "../../.." + end +end diff --git a/packages/react-native/ReactCommon/react/renderer/consistency/ScopedShadowTreeRevisionLock.h b/packages/react-native/ReactCommon/react/renderer/consistency/ScopedShadowTreeRevisionLock.h new file mode 100644 index 000000000000..2bc68d8a2bef --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/consistency/ScopedShadowTreeRevisionLock.h @@ -0,0 +1,54 @@ +/* + * 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. + */ + +#pragma once + +#include + +namespace facebook::react { + +/** + * This is a RAII class that locks the shadow tree revisions during its + * lifetime. + * + * @example + * { + * ScopedShadowTreeRevisionLock lock(consistencyManager); + * runJavaScriptTask(); // During this execution, the lock will be active. + * } + */ +class ScopedShadowTreeRevisionLock { + public: + explicit ScopedShadowTreeRevisionLock( + ShadowTreeRevisionConsistencyManager* consistencyManager) noexcept + : consistencyManager_(consistencyManager) { + if (consistencyManager_ != nullptr) { + consistencyManager_->lockRevisions(); + } + } + + // Non-movable + ScopedShadowTreeRevisionLock(const ScopedShadowTreeRevisionLock&) = delete; + ScopedShadowTreeRevisionLock(ScopedShadowTreeRevisionLock&&) = delete; + + // Non-copyable + ScopedShadowTreeRevisionLock& operator=(const ScopedShadowTreeRevisionLock&) = + delete; + ScopedShadowTreeRevisionLock& operator=(ScopedShadowTreeRevisionLock&&) = + delete; + + ~ScopedShadowTreeRevisionLock() noexcept { + if (consistencyManager_ != nullptr) { + consistencyManager_->unlockRevisions(); + } + } + + private: + ShadowTreeRevisionConsistencyManager* consistencyManager_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/consistency/ShadowTreeRevisionConsistencyManager.cpp b/packages/react-native/ReactCommon/react/renderer/consistency/ShadowTreeRevisionConsistencyManager.cpp new file mode 100644 index 000000000000..686a0bfc0c8c --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/consistency/ShadowTreeRevisionConsistencyManager.cpp @@ -0,0 +1,14 @@ +/* + * 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 "ShadowTreeRevisionConsistencyManager.h" + +namespace facebook::react { + +// This is a placeholder for CMakeFile not to complain + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/consistency/ShadowTreeRevisionConsistencyManager.h b/packages/react-native/ReactCommon/react/renderer/consistency/ShadowTreeRevisionConsistencyManager.h new file mode 100644 index 000000000000..d5afba016f64 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/consistency/ShadowTreeRevisionConsistencyManager.h @@ -0,0 +1,24 @@ +/* + * 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. + */ + +#pragma once + +namespace facebook::react { + +/** + * This interface is used for UI consistency, indicating the timeframe where + * users should see the same revision of the shadow tree. + */ +class ShadowTreeRevisionConsistencyManager { + public: + virtual ~ShadowTreeRevisionConsistencyManager() = default; + + virtual void lockRevisions() = 0; + virtual void unlockRevisions() = 0; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/CMakeLists.txt index 85f9f2c125ff..9149d39eeabe 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/CMakeLists.txt @@ -23,6 +23,7 @@ target_link_libraries(react_render_runtimescheduler callinvoker jsi react_debug + react_render_consistency react_render_debug react_utils react_featureflags diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/React-runtimescheduler.podspec b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/React-runtimescheduler.podspec index 642d71980a94..739502a89036 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/React-runtimescheduler.podspec +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/React-runtimescheduler.podspec @@ -60,6 +60,7 @@ Pod::Spec.new do |s| s.dependency "glog" s.dependency "RCT-Folly", folly_version s.dependency "React-jsi" + s.dependency "React-rendererconsistency" add_dependency(s, "React-debug") if ENV["USE_HERMES"] == nil || ENV["USE_HERMES"] == "1" diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp index 4f2cf5d8646d..ffad447dc366 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.cpp @@ -84,4 +84,11 @@ void RuntimeScheduler::scheduleRenderingUpdate( std::move(renderingUpdate)); } +void RuntimeScheduler::setShadowTreeRevisionConsistencyManager( + ShadowTreeRevisionConsistencyManager* + shadowTreeRevisionConsistencyManager) { + return runtimeSchedulerImpl_->setShadowTreeRevisionConsistencyManager( + shadowTreeRevisionConsistencyManager); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h index 115bc3ec3d99..cdb321cca02a 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include @@ -35,6 +36,8 @@ class RuntimeSchedulerBase { virtual void callExpiredTasks(jsi::Runtime& runtime) = 0; virtual void scheduleRenderingUpdate( RuntimeSchedulerRenderingUpdate&& renderingUpdate) = 0; + virtual void setShadowTreeRevisionConsistencyManager( + ShadowTreeRevisionConsistencyManager* provider) = 0; }; // This is a proxy for RuntimeScheduler implementation, which will be selected @@ -127,6 +130,10 @@ class RuntimeScheduler final : RuntimeSchedulerBase { void scheduleRenderingUpdate( RuntimeSchedulerRenderingUpdate&& renderingUpdate) override; + void setShadowTreeRevisionConsistencyManager( + ShadowTreeRevisionConsistencyManager* + shadowTreeRevisionConsistencyManager) override; + private: // Actual implementation, stored as a unique pointer to simplify memory // management. diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp index 325de9f44c27..2d13b9b8e645 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp @@ -8,6 +8,7 @@ #include "RuntimeScheduler_Legacy.h" #include "SchedulerPriorityUtils.h" +#include #include #include #include "ErrorUtils.h" @@ -30,7 +31,11 @@ void RuntimeScheduler_Legacy::scheduleWork(RawCallback&& callback) noexcept { [this, callback = std::move(callback)](jsi::Runtime& runtime) { SystraceSection s2("RuntimeScheduler::scheduleWork callback"); runtimeAccessRequests_ -= 1; - callback(runtime); + { + ScopedShadowTreeRevisionLock revisionLock( + shadowTreeRevisionConsistencyManager_); + callback(runtime); + } startWorkLoop(runtime); }); } @@ -104,7 +109,11 @@ void RuntimeScheduler_Legacy::executeNowOnTheSameThread( "RuntimeScheduler::executeNowOnTheSameThread callback"); runtimeAccessRequests_ -= 1; - callback(runtime); + { + ScopedShadowTreeRevisionLock revisionLock( + shadowTreeRevisionConsistencyManager_); + callback(runtime); + } }); // Resume work loop if needed. In synchronous mode @@ -145,6 +154,12 @@ void RuntimeScheduler_Legacy::scheduleRenderingUpdate( } } +void RuntimeScheduler_Legacy::setShadowTreeRevisionConsistencyManager( + ShadowTreeRevisionConsistencyManager* + shadowTreeRevisionConsistencyManager) { + shadowTreeRevisionConsistencyManager_ = shadowTreeRevisionConsistencyManager; +} + #pragma mark - Private void RuntimeScheduler_Legacy::scheduleWorkLoopIfNecessary() { @@ -195,13 +210,19 @@ void RuntimeScheduler_Legacy::executeTask( didUserCallbackTimeout); currentPriority_ = task->priority; - auto result = task->execute(runtime, didUserCallbackTimeout); - if (result.isObject() && result.getObject(runtime).isFunction(runtime)) { - task->callback = result.getObject(runtime).getFunction(runtime); - } else { - if (taskQueue_.top() == task) { - taskQueue_.pop(); + { + ScopedShadowTreeRevisionLock revisionLock( + shadowTreeRevisionConsistencyManager_); + + auto result = task->execute(runtime, didUserCallbackTimeout); + + if (result.isObject() && result.getObject(runtime).isFunction(runtime)) { + task->callback = result.getObject(runtime).getFunction(runtime); + } else { + if (taskQueue_.top() == task) { + taskQueue_.pop(); + } } } } diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h index fc436b5d2283..d5f673d39b66 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include @@ -105,6 +106,10 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase { void scheduleRenderingUpdate( RuntimeSchedulerRenderingUpdate&& renderingUpdate) override; + void setShadowTreeRevisionConsistencyManager( + ShadowTreeRevisionConsistencyManager* + shadowTreeRevisionConsistencyManager) override; + private: std::priority_queue< std::shared_ptr, @@ -151,6 +156,9 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase { * This flag is set while performing work, to prevent re-entrancy. */ std::atomic_bool isPerformingWork_{false}; + + ShadowTreeRevisionConsistencyManager* shadowTreeRevisionConsistencyManager_{ + nullptr}; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp index 76011d0a272f..c07e2f8c2e9c 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -153,6 +154,12 @@ void RuntimeScheduler_Modern::scheduleRenderingUpdate( } } +void RuntimeScheduler_Modern::setShadowTreeRevisionConsistencyManager( + ShadowTreeRevisionConsistencyManager* + shadowTreeRevisionConsistencyManager) { + shadowTreeRevisionConsistencyManager_ = shadowTreeRevisionConsistencyManager; +} + #pragma mark - Private void RuntimeScheduler_Modern::scheduleTask(std::shared_ptr task) { @@ -253,16 +260,21 @@ void RuntimeScheduler_Modern::executeTask( currentTask_ = task; currentPriority_ = task->priority; - executeMacrotask(runtime, task, didUserCallbackTimeout); + { + ScopedShadowTreeRevisionLock revisionLock( + shadowTreeRevisionConsistencyManager_); - if (ReactNativeFeatureFlags::enableMicrotasks()) { - // "Perform a microtask checkpoint" step. - performMicrotaskCheckpoint(runtime); - } + executeMacrotask(runtime, task, didUserCallbackTimeout); - if (ReactNativeFeatureFlags::batchRenderingUpdatesInEventLoop()) { - // "Update the rendering" step. - updateRendering(); + if (ReactNativeFeatureFlags::enableMicrotasks()) { + // "Perform a microtask checkpoint" step. + performMicrotaskCheckpoint(runtime); + } + + if (ReactNativeFeatureFlags::batchRenderingUpdatesInEventLoop()) { + // "Update the rendering" step. + updateRendering(); + } } } diff --git a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h index d31707f5217c..372d8c5da19a 100644 --- a/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h +++ b/packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Modern.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include @@ -122,6 +123,10 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { void scheduleRenderingUpdate( RuntimeSchedulerRenderingUpdate&& renderingUpdate) override; + void setShadowTreeRevisionConsistencyManager( + ShadowTreeRevisionConsistencyManager* + shadowTreeRevisionConsistencyManager) override; + private: std::atomic syncTaskRequests_{0}; @@ -184,6 +189,8 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase { bool isWorkLoopScheduled_{false}; std::queue pendingRenderingUpdates_; + ShadowTreeRevisionConsistencyManager* shadowTreeRevisionConsistencyManager_{ + nullptr}; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt index 38fdcb66e863..44fda8bc85fc 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/CMakeLists.txt @@ -25,6 +25,7 @@ target_link_libraries(react_render_scheduler jsi react_config react_debug + react_featureflags react_render_componentregistry react_render_core react_render_debug diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 908e01904dd5..9f5dc0e75f4c 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -54,6 +55,11 @@ Scheduler::Scheduler( ? weakRuntimeScheduler.value().lock() : nullptr; + if (runtimeScheduler && ReactNativeFeatureFlags::enableUIConsistency()) { + runtimeScheduler->setShadowTreeRevisionConsistencyManager( + uiManager->getShadowTreeRevisionConsistencyManager()); + } + auto eventPipe = [uiManager, runtimeScheduler = runtimeScheduler.get()]( jsi::Runtime& runtime, const EventTarget* eventTarget, diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt index badeb64afcfd..c57725e3e638 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt @@ -27,6 +27,8 @@ target_link_libraries(react_render_uimanager react_debug react_featureflags react_render_componentregistry + react_render_consistency + react_render_uimanager_consistency react_render_core react_render_debug react_render_graphics diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 0ad24461223a..4f000b89246a 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -29,6 +29,15 @@ constexpr int DOCUMENT_POSITION_PRECEDING = 2; constexpr int DOCUMENT_POSITION_FOLLOWING = 4; constexpr int DOCUMENT_POSITION_CONTAINS = 8; constexpr int DOCUMENT_POSITION_CONTAINED_BY = 16; + +std::unique_ptr constructLeakCheckerIfNeeded( + const facebook::react::RuntimeExecutor& runtimeExecutor) { +#ifdef REACT_NATIVE_DEBUG + return std::make_unique(runtimeExecutor); +#else + return {}; +#endif +} } // namespace namespace facebook::react { @@ -39,23 +48,25 @@ namespace facebook::react { // isHostObject method) ShadowNodeListWrapper::~ShadowNodeListWrapper() = default; -static std::unique_ptr constructLeakCheckerIfNeeded( - const RuntimeExecutor& runtimeExecutor) { -#ifdef REACT_NATIVE_DEBUG - return std::make_unique(runtimeExecutor); -#else - return {}; -#endif -} - UIManager::UIManager( const RuntimeExecutor& runtimeExecutor, BackgroundExecutor backgroundExecutor, ContextContainer::Shared contextContainer) : runtimeExecutor_(runtimeExecutor), + shadowTreeRegistry_(), backgroundExecutor_(std::move(backgroundExecutor)), contextContainer_(std::move(contextContainer)), - leakChecker_(constructLeakCheckerIfNeeded(runtimeExecutor)) {} + leakChecker_(constructLeakCheckerIfNeeded(runtimeExecutor)), + lazyShadowTreeRevisionConsistencyManager_( + ReactNativeFeatureFlags::enableUIConsistency() + ? std::make_unique( + shadowTreeRegistry_) + : nullptr), + latestShadowTreeRevisionProvider_( + ReactNativeFeatureFlags::enableUIConsistency() + ? nullptr + : std::make_unique( + shadowTreeRegistry_)) {} UIManager::~UIManager() { LOG(WARNING) << "UIManager::~UIManager() was called (address: " << this @@ -166,11 +177,11 @@ void UIManager::appendChild( void UIManager::completeSurface( SurfaceId surfaceId, const ShadowNode::UnsharedListOfShared& rootChildren, - ShadowTree::CommitOptions commitOptions) const { + ShadowTree::CommitOptions commitOptions) { SystraceSection s("UIManager::completeSurface", "surfaceId", surfaceId); shadowTreeRegistry_.visit(surfaceId, [&](const ShadowTree& shadowTree) { - shadowTree.commit( + auto result = shadowTree.commit( [&](RootShadowNode const& oldRootShadowNode) { return std::make_shared( oldRootShadowNode, @@ -180,6 +191,14 @@ void UIManager::completeSurface( }); }, commitOptions); + + if (result == ShadowTree::CommitStatus::Succeeded && + lazyShadowTreeRevisionConsistencyManager_ != nullptr) { + // It's safe to update the visible revision of the shadow tree immediately + // after we commit a specific one. + lazyShadowTreeRevisionConsistencyManager_->updateCurrentRevision( + surfaceId, shadowTree.getCurrentRevision().rootShadowNode); + } }); } @@ -408,6 +427,24 @@ int UIManager::compareDocumentPosition( return DOCUMENT_POSITION_FOLLOWING; } +ShadowTreeRevisionConsistencyManager* +UIManager::getShadowTreeRevisionConsistencyManager() { + return lazyShadowTreeRevisionConsistencyManager_.get(); +} + +ShadowTreeRevisionProvider* UIManager::getShadowTreeRevisionProvider() { + if (lazyShadowTreeRevisionConsistencyManager_ != nullptr) { + return lazyShadowTreeRevisionConsistencyManager_.get(); + } else if (latestShadowTreeRevisionProvider_ != nullptr) { + return latestShadowTreeRevisionProvider_.get(); + } + + LOG(ERROR) << "Unexpected state found in UIManager where both " + << "lazyShadowTreeRevisionConsistencyManager_ and " + << "latestShadowTreeRevisionProvider_ were null"; + return nullptr; +} + ShadowNode::Shared UIManager::findNodeAtPoint( const ShadowNode::Shared& node, Point point) const { diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index c0f443ff568e..936b963a4cee 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -24,6 +25,9 @@ #include #include #include +#include +#include +#include #include #include @@ -106,6 +110,10 @@ class UIManager final : public ShadowTreeDelegate { const ShadowNode& shadowNode, const ShadowNode& otherShadowNode) const; + ShadowTreeRevisionConsistencyManager* + getShadowTreeRevisionConsistencyManager(); + ShadowTreeRevisionProvider* getShadowTreeRevisionProvider(); + #pragma mark - Surface Start & Stop void startSurface( @@ -152,7 +160,7 @@ class UIManager final : public ShadowTreeDelegate { void completeSurface( SurfaceId surfaceId, const ShadowNode::UnsharedListOfShared& rootChildren, - ShadowTree::CommitOptions commitOptions) const; + ShadowTree::CommitOptions commitOptions); void setIsJSResponder( const ShadowNode::Shared& shadowNode, @@ -238,6 +246,11 @@ class UIManager final : public ShadowTreeDelegate { mutable std::vector mountHooks_; std::unique_ptr leakChecker_; + + std::unique_ptr + lazyShadowTreeRevisionConsistencyManager_; + std::unique_ptr + latestShadowTreeRevisionProvider_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/CMakeLists.txt new file mode 100644 index 000000000000..543713ef0b81 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/CMakeLists.txt @@ -0,0 +1,26 @@ +# 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. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +add_compile_options( + -fexceptions + -frtti + -std=c++20 + -Wall + -Wpedantic + -DLOG_TAG=\"Fabric\") + +file(GLOB react_render_uimanager_consistency_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_render_uimanager_consistency SHARED ${react_render_uimanager_consistency_SRC}) + +target_include_directories(react_render_uimanager_consistency PUBLIC ${REACT_COMMON_DIR}) + +target_link_libraries(react_render_uimanager_consistency + glog + rrc_root + react_render_consistency + react_render_mounting) diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LatestShadowTreeRevisionProvider.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LatestShadowTreeRevisionProvider.cpp new file mode 100644 index 000000000000..3bf77f2a9be9 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LatestShadowTreeRevisionProvider.cpp @@ -0,0 +1,29 @@ +/* + * 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 "LatestShadowTreeRevisionProvider.h" + +namespace facebook::react { + +LatestShadowTreeRevisionProvider::LatestShadowTreeRevisionProvider( + ShadowTreeRegistry& shadowTreeRegistry) + : shadowTreeRegistry_(shadowTreeRegistry) {} + +#pragma mark - ShadowTreeRevisionProvider + +RootShadowNode::Shared LatestShadowTreeRevisionProvider::getCurrentRevision( + SurfaceId surfaceId) { + RootShadowNode::Shared rootShadowNode; + + shadowTreeRegistry_.visit(surfaceId, [&](const ShadowTree& shadowTree) { + rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode; + }); + + return rootShadowNode; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LatestShadowTreeRevisionProvider.h b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LatestShadowTreeRevisionProvider.h new file mode 100644 index 000000000000..30b688e1ca43 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LatestShadowTreeRevisionProvider.h @@ -0,0 +1,35 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +namespace facebook::react { + +/** + * This is a drop-in replacement for `LazyShadowTreeRevisionConsistencyManager` + * that preserves the current behavior (always providing the latest committed + * revision instead of locking to a specific one). + */ +class LatestShadowTreeRevisionProvider : public ShadowTreeRevisionProvider { + public: + explicit LatestShadowTreeRevisionProvider( + ShadowTreeRegistry& shadowTreeRegistry); + +#pragma mark - ShadowTreeRevisionProvider + + RootShadowNode::Shared getCurrentRevision(SurfaceId surfaceId) override; + + private: + ShadowTreeRegistry& shadowTreeRegistry_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.cpp new file mode 100644 index 000000000000..c195a9b91369 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.cpp @@ -0,0 +1,72 @@ +/* + * 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 "LazyShadowTreeRevisionConsistencyManager.h" +#include + +namespace facebook::react { + +LazyShadowTreeRevisionConsistencyManager:: + LazyShadowTreeRevisionConsistencyManager( + ShadowTreeRegistry& shadowTreeRegistry) + : shadowTreeRegistry_(shadowTreeRegistry) {} + +void LazyShadowTreeRevisionConsistencyManager::updateCurrentRevision( + SurfaceId surfaceId, + RootShadowNode::Shared rootShadowNode) { + capturedRootShadowNodesForConsistency_.emplace( + surfaceId, std::move(rootShadowNode)); +} + +#pragma mark - ShadowTreeRevisionProvider + +RootShadowNode::Shared +LazyShadowTreeRevisionConsistencyManager::getCurrentRevision( + SurfaceId surfaceId) { + auto it = capturedRootShadowNodesForConsistency_.find(surfaceId); + if (it != capturedRootShadowNodesForConsistency_.end()) { + return it->second; + } + + RootShadowNode::Shared rootShadowNode; + + shadowTreeRegistry_.visit(surfaceId, [&](const ShadowTree& shadowTree) { + rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode; + }); + + capturedRootShadowNodesForConsistency_.emplace(surfaceId, rootShadowNode); + + return rootShadowNode; +} + +#pragma mark - ConsistentShadowTreeRevisionProvider + +void LazyShadowTreeRevisionConsistencyManager::lockRevisions() { + if (isLocked_) { + LOG(WARNING) + << "LazyShadowTreeRevisionConsistencyManager::lockRevisions() called without unlocking a previous lock"; + return; + } + + // We actually capture the state lazily the first time we access it, so we + // don't need to do anything here. + isLocked_ = true; +} + +void LazyShadowTreeRevisionConsistencyManager::unlockRevisions() { + if (!isLocked_) { + LOG(WARNING) + << "LazyShadowTreeRevisionConsistencyManager::unlockRevisions() called without a previous lock"; + // We don't return here because we want to do the cleanup anyway + // to free up resources. + } + + isLocked_ = false; + capturedRootShadowNodesForConsistency_.clear(); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.h new file mode 100644 index 000000000000..d507263cce55 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/LazyShadowTreeRevisionConsistencyManager.h @@ -0,0 +1,51 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook::react { + +/** + * This class implements UI consistency for the JavaScript thread. + * This implementation forces JavaScript to see a stable revision of the shadow + * tree for a given surface ID, only updating it when React commits a new tree + * or between JS tasks. + */ +class LazyShadowTreeRevisionConsistencyManager + : public ShadowTreeRevisionConsistencyManager, + public ShadowTreeRevisionProvider { + public: + explicit LazyShadowTreeRevisionConsistencyManager( + ShadowTreeRegistry& shadowTreeRegistry); + + void updateCurrentRevision( + SurfaceId surfaceId, + RootShadowNode::Shared rootShadowNode); + +#pragma mark - ShadowTreeRevisionProvider + + RootShadowNode::Shared getCurrentRevision(SurfaceId surfaceId) override; + +#pragma mark - ShadowTreeRevisionConsistencyManager + + void lockRevisions() override; + void unlockRevisions() override; + + private: + std::unordered_map + capturedRootShadowNodesForConsistency_; + ShadowTreeRegistry& shadowTreeRegistry_; + bool isLocked_{false}; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/ShadowTreeRevisionProvider.h b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/ShadowTreeRevisionProvider.h new file mode 100644 index 000000000000..66c36d4aa916 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/consistency/ShadowTreeRevisionProvider.h @@ -0,0 +1,27 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +/** + * This interface is used for UI consistency, indicating the revision of the + * shadow tree that a caller should have access to. + */ +class ShadowTreeRevisionProvider { + public: + virtual ~ShadowTreeRevisionProvider() = default; + + virtual RootShadowNode::Shared getCurrentRevision(SurfaceId surfaceId) = 0; +}; + +} // namespace facebook::react diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index bec7fcf7f234..91e3dd69c8f3 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -67,6 +67,11 @@ const definitions: FeatureFlagDefinitions = { description: 'Uses new, deduplicated logic for constructing Android Spannables from text fragments', }, + enableUIConsistency: { + defaultValue: false, + description: + 'Ensures that JavaScript always has a consistent view of the state of the UI (e.g.: commits done in other threads are not immediately propagated to JS during its execution).', + }, inspectorEnableCxxInspectorPackagerConnection: { defaultValue: false, description: diff --git a/packages/react-native/scripts/react_native_pods.rb b/packages/react-native/scripts/react_native_pods.rb index 462048d89962..cacfbda1ffe7 100644 --- a/packages/react-native/scripts/react_native_pods.rb +++ b/packages/react-native/scripts/react_native_pods.rb @@ -147,6 +147,7 @@ def use_react_native! ( pod 'React-runtimeexecutor', :path => "#{prefix}/ReactCommon/runtimeexecutor" pod 'React-runtimescheduler', :path => "#{prefix}/ReactCommon/react/renderer/runtimescheduler" pod 'React-rendererdebug', :path => "#{prefix}/ReactCommon/react/renderer/debug" + pod 'React-rendererconsistency', :path => "#{prefix}/ReactCommon/react/renderer/consistency" pod 'React-perflogger', :path => "#{prefix}/ReactCommon/reactperflogger" pod 'React-logger', :path => "#{prefix}/ReactCommon/logger" pod 'ReactCommon/turbomodule/core', :path => "#{prefix}/ReactCommon", :modular_headers => true diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index 3d68cd7572a3..dffe28a86f1b 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<> + * @generated SignedSource<> * @flow strict-local */ @@ -48,6 +48,7 @@ export type ReactNativeFeatureFlags = { enableMicrotasks: Getter, enableMountHooksAndroid: Getter, enableSpannableBuildingUnification: Getter, + enableUIConsistency: Getter, inspectorEnableCxxInspectorPackagerConnection: Getter, inspectorEnableModernCDPRegistry: Getter, useModernRuntimeScheduler: Getter, @@ -125,6 +126,10 @@ export const enableMountHooksAndroid: Getter = createNativeFlagGetter(' * Uses new, deduplicated logic for constructing Android Spannables from text fragments */ export const enableSpannableBuildingUnification: Getter = createNativeFlagGetter('enableSpannableBuildingUnification', false); +/** + * Ensures that JavaScript always has a consistent view of the state of the UI (e.g.: commits done in other threads are not immediately propagated to JS during its execution). + */ +export const enableUIConsistency: Getter = createNativeFlagGetter('enableUIConsistency', false); /** * Flag determining if the C++ implementation of InspectorPackagerConnection should be used instead of the per-platform one. This flag is global and should not be changed across React Host lifetimes. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 24c095337f81..ac17e38ee024 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<<8fcd655a9837ad155fd71efe8b05e3d0>> + * @generated SignedSource<<2f4c06a0e456d55a4bbd6b8c48c7f22d>> * @flow strict-local */ @@ -31,6 +31,7 @@ export interface Spec extends TurboModule { +enableMicrotasks?: () => boolean; +enableMountHooksAndroid?: () => boolean; +enableSpannableBuildingUnification?: () => boolean; + +enableUIConsistency?: () => boolean; +inspectorEnableCxxInspectorPackagerConnection?: () => boolean; +inspectorEnableModernCDPRegistry?: () => boolean; +useModernRuntimeScheduler?: () => boolean; From ef28086d45939bd4018fed62c08f766564b99ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 25 Mar 2024 05:06:27 -0700 Subject: [PATCH 2/3] Implement UI consistency in DOM methods Differential Revision: D55077311 --- .../ReactAndroid/build.gradle.kts | 1 + .../ReactAndroid/src/main/jni/CMakeLists.txt | 1 + .../ReactCommon/React-Fabric.podspec | 13 + .../react/nativemodule/dom/NativeDOM.cpp | 272 ++++------ .../react/renderer/dom/CMakeLists.txt | 26 + .../ReactCommon/react/renderer/dom/DOM.cpp | 489 ++++++++++++++++++ .../ReactCommon/react/renderer/dom/DOM.h | 87 ++++ .../react/renderer/uimanager/CMakeLists.txt | 1 + .../react/renderer/uimanager/UIManager.cpp | 138 ----- .../react/renderer/uimanager/UIManager.h | 13 - .../renderer/uimanager/UIManagerBinding.cpp | 44 +- .../react/renderer/uimanager/primitives.h | 69 --- 12 files changed, 744 insertions(+), 410 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/dom/CMakeLists.txt create mode 100644 packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/dom/DOM.h diff --git a/packages/react-native/ReactAndroid/build.gradle.kts b/packages/react-native/ReactAndroid/build.gradle.kts index 3a73ee13681b..989df452c3aa 100644 --- a/packages/react-native/ReactAndroid/build.gradle.kts +++ b/packages/react-native/ReactAndroid/build.gradle.kts @@ -564,6 +564,7 @@ android { "react_render_animations", "react_render_core", "react_render_consistency", + "react_render_dom", "react_render_graphics", "rrc_image", "rrc_root", diff --git a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt index 5c0d6a9cd7fa..1e0a86425d98 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt +++ b/packages/react-native/ReactAndroid/src/main/jni/CMakeLists.txt @@ -81,6 +81,7 @@ add_react_common_subdir(react/renderer/uimanager) add_react_common_subdir(react/renderer/core) add_react_common_subdir(react/renderer/consistency) add_react_common_subdir(react/renderer/uimanager/consistency) +add_react_common_subdir(react/renderer/dom) add_react_common_subdir(react/renderer/element) add_react_common_subdir(react/renderer/graphics) add_react_common_subdir(react/renderer/debug) diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index 05d0e7259e59..d7af3b162a30 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -286,11 +286,24 @@ Pod::Spec.new do |s| ss.dependency folly_dep_name, folly_version ss.dependency "React-rendererconsistency" + ss.dependency "React-Fabric/dom" ss.compiler_flags = folly_compiler_flags ss.source_files = "react/renderer/uimanager/*.{m,mm,cpp,h}" ss.header_dir = "react/renderer/uimanager" end + s.subspec "dom" do |ss| + ss.dependency folly_dep_name, folly_version + ss.dependency "React-Fabric/components/root" + ss.dependency "React-Fabric/components/text" + ss.dependency "React-Fabric/core" + ss.dependency "React-graphics" + ss.compiler_flags = folly_compiler_flags + ss.source_files = "react/renderer/dom/**/*.{m,mm,cpp,h}" + ss.exclude_files = "react/renderer/dom/tests" + ss.header_dir = "react/renderer/dom" + end + s.subspec "telemetry" do |ss| ss.dependency folly_dep_name, folly_version ss.compiler_flags = folly_compiler_flags diff --git a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp index 2c26e9c415d9..79cf1bf09db3 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp @@ -6,10 +6,11 @@ */ #include "NativeDOM.h" +#include +#include #include #include #include -#include #include #ifdef RN_DISABLE_OSS_PLUGIN_HEADER @@ -22,9 +23,15 @@ std::shared_ptr NativeDOMModuleProvider( } namespace { -facebook::react::UIManager& getUIManagerFromRuntime( - facebook::jsi::Runtime& runtime) { - return facebook::react::UIManagerBinding::getBinding(runtime)->getUIManager(); +using namespace facebook::react; + +RootShadowNode::Shared getCurrentShadowTreeRevision( + facebook::jsi::Runtime& runtime, + SurfaceId surfaceId) { + auto& uiManager = + facebook::react::UIManagerBinding::getBinding(runtime)->getUIManager(); + auto shadowTreeRevisionProvider = uiManager.getShadowTreeRevisionProvider(); + return shadowTreeRevisionProvider->getCurrentRevision(surfaceId); } facebook::react::PointerEventsProcessor& getPointerEventsProcessorFromRuntime( @@ -32,6 +39,24 @@ facebook::react::PointerEventsProcessor& getPointerEventsProcessorFromRuntime( return facebook::react::UIManagerBinding::getBinding(runtime) ->getPointerEventsProcessor(); } + +std::vector getArrayOfInstanceHandlesFromShadowNodes( + const ShadowNode::ListOfShared& nodes, + facebook::jsi::Runtime& runtime) { + // JSI doesn't support adding elements to an array after creation, + // so we need to accumulate the values in a vector and then create + // the array when we know the size. + std::vector nonNullInstanceHandles; + nonNullInstanceHandles.reserve(nodes.size()); + for (const auto& shadowNode : nodes) { + auto instanceHandle = (*shadowNode).getInstanceHandle(runtime); + if (!instanceHandle.isNull()) { + nonNullInstanceHandles.push_back(std::move(instanceHandle)); + } + } + + return nonNullInstanceHandles; +} } // namespace namespace facebook::react { @@ -43,41 +68,49 @@ std::optional NativeDOM::getParentNode( jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - auto parentShadowNode = - getUIManagerFromRuntime(rt).getNewestParentOfShadowNode(*shadowNode); + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return std::nullopt; + } - // shadowNode is a RootShadowNode - if (!parentShadowNode) { + auto parentShadowNode = dom::getParentNode(currentRevision, *shadowNode); + if (parentShadowNode == nullptr) { return std::nullopt; } - return (*parentShadowNode).getInstanceHandle(rt); + return parentShadowNode->getInstanceHandle(rt); } std::optional> NativeDOM::getChildNodes( jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return std::nullopt; + } - auto newestCloneOfShadowNode = - getUIManagerFromRuntime(rt).getNewestCloneOfShadowNode(*shadowNode); + auto childNodes = dom::getChildNodes(currentRevision, *shadowNode); // There's no version of this node in the current shadow tree - if (newestCloneOfShadowNode == nullptr) { + if (!childNodes) { return std::nullopt; } - auto childShadowNodes = newestCloneOfShadowNode->getChildren(); - return getArrayOfInstanceHandlesFromShadowNodes(childShadowNodes, rt); + return getArrayOfInstanceHandlesFromShadowNodes(childNodes.value(), rt); } bool NativeDOM::isConnected(jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return false; + } - auto newestCloneOfShadowNode = - getUIManagerFromRuntime(rt).getNewestCloneOfShadowNode(*shadowNode); - - return newestCloneOfShadowNode != nullptr; + return dom::isConnected(currentRevision, *shadowNode); } double NativeDOM::compareDocumentPosition( @@ -86,23 +119,27 @@ double NativeDOM::compareDocumentPosition( jsi::Value otherShadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); auto otherShadowNode = shadowNodeFromValue(rt, otherShadowNodeValue); + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (otherShadowNode == nullptr || currentRevision == nullptr) { + return 0; + } - auto documentPosition = getUIManagerFromRuntime(rt).compareDocumentPosition( - *shadowNode, *otherShadowNode); - - return documentPosition; + return dom::compareDocumentPosition( + currentRevision, *shadowNode, *otherShadowNode); } std::string NativeDOM::getTextContent( jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + return ""; + } - auto textContent = - getUIManagerFromRuntime(rt).getTextContentInNewestCloneOfShadowNode( - *shadowNode); - - return textContent; + return dom::getTextContent(currentRevision, *shadowNode); } std::optionalgetSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - auto frame = layoutMetrics.frame; - return std::tuple{ - frame.origin.x, frame.origin.y, frame.size.width, frame.size.height}; + return dom::getBoundingClientRect( + currentRevision, *shadowNode, includeTransform); } std::optional> NativeDOM::getOffset(jsi::Runtime& rt, jsi::Value shadowNodeValue) { - auto& uiManager = getUIManagerFromRuntime(rt); auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - - auto newestCloneOfShadowNode = - uiManager.getNewestCloneOfShadowNode(*shadowNode); - auto newestPositionedAncestorOfShadowNode = - uiManager.getNewestPositionedAncestorOfShadowNode(*shadowNode); - // The node is no longer part of an active shadow tree, or it is the - // root node - if (newestCloneOfShadowNode == nullptr || - newestPositionedAncestorOfShadowNode == nullptr) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto shadowNodeLayoutMetricsRelativeToRoot = - uiManager.getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ false}); - if (shadowNodeLayoutMetricsRelativeToRoot == EmptyLayoutMetrics) { - return std::nullopt; - } + auto offset = dom::getOffset(currentRevision, *shadowNode); - auto positionedAncestorLayoutMetricsRelativeToRoot = - uiManager.getRelativeLayoutMetrics( - *newestPositionedAncestorOfShadowNode, - nullptr, - {/* .includeTransform = */ false}); - if (positionedAncestorLayoutMetricsRelativeToRoot == EmptyLayoutMetrics) { + if (!offset) { return std::nullopt; } - auto shadowNodeOriginRelativeToRoot = - shadowNodeLayoutMetricsRelativeToRoot.frame.origin; - auto positionedAncestorOriginRelativeToRoot = - positionedAncestorLayoutMetricsRelativeToRoot.frame.origin; - - // On the Web, offsets are computed from the inner border of the - // parent. - auto offsetTop = shadowNodeOriginRelativeToRoot.y - - positionedAncestorOriginRelativeToRoot.y - - positionedAncestorLayoutMetricsRelativeToRoot.borderWidth.top; - auto offsetLeft = shadowNodeOriginRelativeToRoot.x - - positionedAncestorOriginRelativeToRoot.x - - positionedAncestorLayoutMetricsRelativeToRoot.borderWidth.left; + auto& offsetValue = offset.value(); return std::tuple{ - (*newestPositionedAncestorOfShadowNode).getInstanceHandle(rt), - offsetTop, - offsetLeft}; + std::get<0>(offsetValue)->getInstanceHandle(rt), + std::get<1>(offsetValue), + std::get<2>(offsetValue)}; } std::optional> NativeDOM::getScrollPosition(jsi::Runtime& rt, jsi::Value shadowNodeValue) { - auto& uiManager = getUIManagerFromRuntime(rt); auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - - auto newestCloneOfShadowNode = - uiManager.getNewestCloneOfShadowNode(*shadowNode); - // The node is no longer part of an active shadow tree, or it is the - // root node - if (newestCloneOfShadowNode == nullptr) { - return std::nullopt; - } - - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = uiManager.getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ true}); - - if (layoutMetrics == EmptyLayoutMetrics) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - auto layoutableShadowNode = - dynamic_cast(newestCloneOfShadowNode.get()); - // This should never happen - if (layoutableShadowNode == nullptr) { - return std::nullopt; - } - - auto scrollPosition = layoutableShadowNode->getContentOriginOffset(); - - return std::tuple{ - scrollPosition.x == 0 ? 0 : -scrollPosition.x, - scrollPosition.y == 0 ? 0 : -scrollPosition.y}; + return dom::getScrollPosition(currentRevision, *shadowNode); } std::optional> NativeDOM::getScrollSize(jsi::Runtime& rt, jsi::Value shadowNodeValue) { - auto& uiManager = getUIManagerFromRuntime(rt); auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - - auto newestCloneOfShadowNode = - uiManager.getNewestCloneOfShadowNode(*shadowNode); - // The node is no longer part of an active shadow tree, or it is the - // root node - if (newestCloneOfShadowNode == nullptr) { - return std::nullopt; - } - - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = uiManager.getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ false}); - - if (layoutMetrics == EmptyLayoutMetrics || - layoutMetrics.displayType == DisplayType::Inline) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - auto layoutableShadowNode = dynamic_cast( - newestCloneOfShadowNode.get()); - // This should never happen - if (layoutableShadowNode == nullptr) { - return std::nullopt; - } - - Size scrollSize = getScrollableContentBounds( - layoutableShadowNode->getContentBounds(), layoutMetrics) - .size; - - return std::tuple{ - std::round(scrollSize.width), std::round(scrollSize.height)}; + return dom::getScrollSize(currentRevision, *shadowNode); } std::optional> NativeDOM::getInnerSize(jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = getUIManagerFromRuntime(rt).getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ false}); - - if (layoutMetrics == EmptyLayoutMetrics || - layoutMetrics.displayType == DisplayType::Inline) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - auto paddingFrame = layoutMetrics.getPaddingFrame(); - - return std::tuple{ - std::round(paddingFrame.size.width), - std::round(paddingFrame.size.height)}; + return dom::getInnerSize(currentRevision, *shadowNode); } std::optional> NativeDOM::getBorderSize(jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - - // If the node is not displayed (itself or any of its ancestors has - // "display: none"), this returns an empty layout metrics object. - auto layoutMetrics = getUIManagerFromRuntime(rt).getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ false}); - - if (layoutMetrics == EmptyLayoutMetrics || - layoutMetrics.displayType == DisplayType::Inline) { + auto currentRevision = + getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { return std::nullopt; } - return std::tuple{ - std::round(layoutMetrics.borderWidth.top), - std::round(layoutMetrics.borderWidth.right), - std::round(layoutMetrics.borderWidth.bottom), - std::round(layoutMetrics.borderWidth.left)}; + return dom::getBorderSize(currentRevision, *shadowNode); } std::string NativeDOM::getTagName( jsi::Runtime& rt, jsi::Value shadowNodeValue) { auto shadowNode = shadowNodeFromValue(rt, shadowNodeValue); - - std::string canonicalComponentName = shadowNode->getComponentName(); - - // FIXME(T162807327): Remove Android-specific prefixes and unify - // shadow node implementations - if (canonicalComponentName == "AndroidTextInput") { - canonicalComponentName = "TextInput"; - } else if (canonicalComponentName == "AndroidSwitch") { - canonicalComponentName = "Switch"; - } - - // Prefix with RN: - canonicalComponentName.insert(0, "RN:"); - - return canonicalComponentName; + return dom::getTagName(*shadowNode); } bool NativeDOM::hasPointerCapture( diff --git a/packages/react-native/ReactCommon/react/renderer/dom/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/dom/CMakeLists.txt new file mode 100644 index 000000000000..2bc7a84189b1 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/dom/CMakeLists.txt @@ -0,0 +1,26 @@ +# 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. + +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +add_compile_options( + -fexceptions + -frtti + -std=c++20 + -Wall + -Wpedantic + -DLOG_TAG=\"Fabric\") + +file(GLOB react_render_dom_SRC CONFIGURE_DEPENDS *.cpp) +add_library(react_render_dom STATIC ${react_render_dom_SRC}) + +target_include_directories(react_render_dom PUBLIC ${REACT_COMMON_DIR}) + +target_link_libraries(react_render_dom + react_render_core + react_render_graphics + rrc_root + rrc_text) diff --git a/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp b/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp new file mode 100644 index 000000000000..f52101b7da7c --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp @@ -0,0 +1,489 @@ +/* + * 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 "DOM.h" +#include +#include +#include +#include +#include + +namespace { + +using namespace facebook::react; + +// To prevent ambiguity with built-in MacOS types. +using facebook::react::Point; +using facebook::react::Rect; +using facebook::react::Size; + +constexpr uint_fast16_t DOCUMENT_POSITION_DISCONNECTED = 1; +constexpr uint_fast16_t DOCUMENT_POSITION_PRECEDING = 2; +constexpr uint_fast16_t DOCUMENT_POSITION_FOLLOWING = 4; +constexpr uint_fast16_t DOCUMENT_POSITION_CONTAINS = 8; +constexpr uint_fast16_t DOCUMENT_POSITION_CONTAINED_BY = 16; + +ShadowNode::Shared getShadowNodeInRevision( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + // If the given shadow node is of the same family as the root shadow node, + // return the latest root shadow node + if (ShadowNode::sameFamily(*currentRevision, shadowNode)) { + return currentRevision; + } + + auto ancestors = shadowNode.getFamily().getAncestors(*currentRevision); + + if (ancestors.empty()) { + return nullptr; + } + + auto pair = ancestors.rbegin(); + return pair->first.get().getChildren().at(pair->second); +} + +ShadowNode::Shared getParentShadowNodeInRevision( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + // If the given shadow node is of the same family as the root shadow node, + // return the latest root shadow node + if (ShadowNode::sameFamily(*currentRevision, shadowNode)) { + return currentRevision; + } + + auto ancestors = shadowNode.getFamily().getAncestors(*currentRevision); + + if (ancestors.empty()) { + return nullptr; + } + + if (ancestors.size() == 1) { + // The parent is the shadow root + return currentRevision; + } + + auto parentOfParentPair = ancestors[ancestors.size() - 2]; + return parentOfParentPair.first.get().getChildren().at( + parentOfParentPair.second); +} + +ShadowNode::Shared getPositionedAncestorOfShadowNodeInRevision( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto ancestors = shadowNode.getFamily().getAncestors(*currentRevision); + + if (ancestors.empty()) { + return nullptr; + } + + for (auto it = ancestors.rbegin(); it != ancestors.rend(); it++) { + const auto layoutableAncestorShadowNode = + dynamic_cast(&(it->first.get())); + if (layoutableAncestorShadowNode == nullptr) { + return nullptr; + } + if (layoutableAncestorShadowNode->getLayoutMetrics().positionType != + PositionType::Static) { + // We have found our nearest positioned ancestor, now to get a shared + // pointer of it + it++; + if (it != ancestors.rend()) { + return it->first.get().getChildren().at(it->second); + } + // else the positioned ancestor is the root which we return outside of the + // loop + } + } + + // If there is no positioned ancestor then we just consider the root + // to be one + return currentRevision; +} + +void getTextContentInShadowNode( + const ShadowNode& shadowNode, + std::string& result) { + auto rawTextShadowNode = dynamic_cast(&shadowNode); + + if (rawTextShadowNode != nullptr) { + result.append(rawTextShadowNode->getConcreteProps().text); + } + + for (const auto& childNode : shadowNode.getChildren()) { + getTextContentInShadowNode(*childNode, result); + } +} + +LayoutMetrics getRelativeLayoutMetrics( + const ShadowNode& ancestorNode, + const ShadowNode& shadowNode, + LayoutableShadowNode::LayoutInspectingPolicy policy) { + auto layoutableAncestorShadowNode = + dynamic_cast(&ancestorNode); + + if (layoutableAncestorShadowNode == nullptr) { + return EmptyLayoutMetrics; + } + + return LayoutableShadowNode::computeRelativeLayoutMetrics( + shadowNode.getFamily(), *layoutableAncestorShadowNode, policy); +} + +Rect getScrollableContentBounds( + Rect contentBounds, + LayoutMetrics layoutMetrics) { + auto paddingFrame = layoutMetrics.getPaddingFrame(); + + auto paddingBottom = + layoutMetrics.contentInsets.bottom - layoutMetrics.borderWidth.bottom; + auto paddingLeft = + layoutMetrics.contentInsets.left - layoutMetrics.borderWidth.left; + auto paddingRight = + layoutMetrics.contentInsets.right - layoutMetrics.borderWidth.right; + + auto minY = paddingFrame.getMinY(); + auto maxY = + std::max(paddingFrame.getMaxY(), contentBounds.getMaxY() + paddingBottom); + + auto minX = layoutMetrics.layoutDirection == LayoutDirection::RightToLeft + ? std::min(paddingFrame.getMinX(), contentBounds.getMinX() - paddingLeft) + : paddingFrame.getMinX(); + auto maxX = layoutMetrics.layoutDirection == LayoutDirection::RightToLeft + ? paddingFrame.getMaxX() + : std::max( + paddingFrame.getMaxX(), contentBounds.getMaxX() + paddingRight); + + return Rect{Point{minX, minY}, Size{maxX - minX, maxY - minY}}; +} + +} // namespace + +namespace facebook::react::dom { + +ShadowNode::Shared getParentNode( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + return getParentShadowNodeInRevision(currentRevision, shadowNode); +} + +std::optional> getChildNodes( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + return shadowNodeInCurrentRevision->getChildren(); +} + +bool isConnected( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + return shadowNodeInCurrentRevision != nullptr; +} + +uint_fast16_t compareDocumentPosition( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode, + const ShadowNode& otherShadowNode) { + if (shadowNode.getSurfaceId() != otherShadowNode.getSurfaceId()) { + return DOCUMENT_POSITION_DISCONNECTED; + } + + // Quick check for node vs. itself + if (&shadowNode == &otherShadowNode) { + return 0; + } + + auto ancestors = shadowNode.getFamily().getAncestors(*currentRevision); + if (ancestors.empty()) { + return DOCUMENT_POSITION_DISCONNECTED; + } + + auto otherAncestors = + otherShadowNode.getFamily().getAncestors(*currentRevision); + if (ancestors.empty()) { + return DOCUMENT_POSITION_DISCONNECTED; + } + + // Consume all common ancestors + size_t i = 0; + while (i < ancestors.size() && i < otherAncestors.size() && + ancestors[i].second == otherAncestors[i].second) { + i++; + } + + if (i == ancestors.size()) { + return (DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING); + } + + if (i == otherAncestors.size()) { + return (DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING); + } + + if (ancestors[i].second > otherAncestors[i].second) { + return DOCUMENT_POSITION_PRECEDING; + } + + return DOCUMENT_POSITION_FOLLOWING; +} + +std::string getTextContent( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + std::string result; + + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + + if (shadowNodeInCurrentRevision != nullptr) { + getTextContentInShadowNode(*shadowNodeInCurrentRevision, result); + } + + return result; +} + +std::optional> +getBoundingClientRect( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode, + bool includeTransform) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + auto layoutMetrics = getRelativeLayoutMetrics( + *currentRevision, + shadowNode, + {.includeTransform = includeTransform, .includeViewportOffset = true}); + + if (layoutMetrics == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto frame = layoutMetrics.frame; + return std::tuple{ + frame.origin.x, frame.origin.y, frame.size.width, frame.size.height}; +} + +std::optional> +getOffset( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + auto positionedAncestorOfShadowNodeInCurrentRevision = + getPositionedAncestorOfShadowNodeInRevision(currentRevision, shadowNode); + + // The node is no longer part of an active shadow tree, or it is the + // root node + if (shadowNodeInCurrentRevision == nullptr || + positionedAncestorOfShadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto shadowNodeLayoutMetricsRelativeToRoot = getRelativeLayoutMetrics( + *currentRevision, shadowNode, {.includeTransform = false}); + if (shadowNodeLayoutMetricsRelativeToRoot == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto positionedAncestorLayoutMetricsRelativeToRoot = getRelativeLayoutMetrics( + *currentRevision, + *positionedAncestorOfShadowNodeInCurrentRevision, + {.includeTransform = false}); + if (positionedAncestorLayoutMetricsRelativeToRoot == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto shadowNodeOriginRelativeToRoot = + shadowNodeLayoutMetricsRelativeToRoot.frame.origin; + auto positionedAncestorOriginRelativeToRoot = + positionedAncestorLayoutMetricsRelativeToRoot.frame.origin; + + // On the Web, offsets are computed from the inner border of the + // parent. + auto offsetTop = shadowNodeOriginRelativeToRoot.y - + positionedAncestorOriginRelativeToRoot.y - + positionedAncestorLayoutMetricsRelativeToRoot.borderWidth.top; + auto offsetLeft = shadowNodeOriginRelativeToRoot.x - + positionedAncestorOriginRelativeToRoot.x - + positionedAncestorLayoutMetricsRelativeToRoot.borderWidth.left; + + return std::tuple{ + positionedAncestorOfShadowNodeInCurrentRevision, offsetTop, offsetLeft}; +} + +std::optional> +getScrollPosition( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto layoutMetrics = getRelativeLayoutMetrics( + *currentRevision, + *shadowNodeInCurrentRevision, + {.includeTransform = true}); + + if (layoutMetrics == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto layoutableShadowNode = dynamic_cast( + shadowNodeInCurrentRevision.get()); + // This should never happen + if (layoutableShadowNode == nullptr) { + return std::nullopt; + } + + auto scrollPosition = layoutableShadowNode->getContentOriginOffset(); + + return std::tuple{ + scrollPosition.x == 0 ? 0 : -scrollPosition.x, + scrollPosition.y == 0 ? 0 : -scrollPosition.y}; +} + +std::optional> +getScrollSize( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto layoutMetrics = getRelativeLayoutMetrics( + *currentRevision, + *shadowNodeInCurrentRevision, + {.includeTransform = false}); + + if (layoutMetrics == EmptyLayoutMetrics || + layoutMetrics.displayType == DisplayType::Inline) { + return std::nullopt; + } + + auto layoutableShadowNode = dynamic_cast( + shadowNodeInCurrentRevision.get()); + // This should never happen + if (layoutableShadowNode == nullptr) { + return std::nullopt; + } + + Size scrollSize = getScrollableContentBounds( + layoutableShadowNode->getContentBounds(), layoutMetrics) + .size; + + return std::tuple{ + std::round(scrollSize.width), std::round(scrollSize.height)}; +} + +std::optional> getInnerSize( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto layoutMetrics = getRelativeLayoutMetrics( + *currentRevision, + *shadowNodeInCurrentRevision, + {.includeTransform = false}); + + if (layoutMetrics == EmptyLayoutMetrics || + layoutMetrics.displayType == DisplayType::Inline) { + return std::nullopt; + } + + auto paddingFrame = layoutMetrics.getPaddingFrame(); + + return std::tuple{ + std::round(paddingFrame.size.width), + std::round(paddingFrame.size.height)}; +} + +std::optional> +getBorderSize( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + // If the node is not displayed (itself or any of its ancestors has + // "display: none"), this returns an empty layout metrics object. + auto layoutMetrics = getRelativeLayoutMetrics( + *currentRevision, + *shadowNodeInCurrentRevision, + {.includeTransform = false}); + + if (layoutMetrics == EmptyLayoutMetrics || + layoutMetrics.displayType == DisplayType::Inline) { + return std::nullopt; + } + + return std::tuple{ + std::round(layoutMetrics.borderWidth.top), + std::round(layoutMetrics.borderWidth.right), + std::round(layoutMetrics.borderWidth.bottom), + std::round(layoutMetrics.borderWidth.left)}; +} + +std::string getTagName(const ShadowNode& shadowNode) { + std::string canonicalComponentName = shadowNode.getComponentName(); + + // FIXME(T162807327): Remove Android-specific prefixes and unify + // shadow node implementations + if (canonicalComponentName == "AndroidTextInput") { + canonicalComponentName = "TextInput"; + } else if (canonicalComponentName == "AndroidSwitch") { + canonicalComponentName = "Switch"; + } + + // Prefix with RN: + canonicalComponentName.insert(0, "RN:"); + + return canonicalComponentName; +} + +} // namespace facebook::react::dom diff --git a/packages/react-native/ReactCommon/react/renderer/dom/DOM.h b/packages/react-native/ReactCommon/react/renderer/dom/DOM.h new file mode 100644 index 000000000000..a922c514842e --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/dom/DOM.h @@ -0,0 +1,87 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react::dom { + +ShadowNode::Shared getParentNode( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> getChildNodes( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +bool isConnected( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +uint_fast16_t compareDocumentPosition( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode, + const ShadowNode& otherShadowNode); + +std::string getTextContent( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> +getBoundingClientRect( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode, + bool includeTransform); + +std::optional> +getOffset( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> +getScrollPosition( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> +getScrollSize( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> getInnerSize( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> +getBorderSize( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::string getTagName(const ShadowNode& shadowNode); + +} // namespace facebook::react::dom diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt index c57725e3e638..15df5f7c984f 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt @@ -31,6 +31,7 @@ target_link_libraries(react_render_uimanager react_render_uimanager_consistency react_render_core react_render_debug + react_render_dom react_render_graphics react_render_leakchecker react_render_runtimescheduler diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 4f000b89246a..6e909a73902b 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -24,12 +24,6 @@ #include namespace { -constexpr int DOCUMENT_POSITION_DISCONNECTED = 1; -constexpr int DOCUMENT_POSITION_PRECEDING = 2; -constexpr int DOCUMENT_POSITION_FOLLOWING = 4; -constexpr int DOCUMENT_POSITION_CONTAINS = 8; -constexpr int DOCUMENT_POSITION_CONTAINED_BY = 16; - std::unique_ptr constructLeakCheckerIfNeeded( const facebook::react::RuntimeExecutor& runtimeExecutor) { #ifdef REACT_NATIVE_DEBUG @@ -295,138 +289,6 @@ ShadowNode::Shared UIManager::getNewestCloneOfShadowNode( return pair->first.get().getChildren().at(pair->second); } -ShadowNode::Shared UIManager::getNewestParentOfShadowNode( - const ShadowNode& shadowNode) const { - auto ancestorShadowNode = ShadowNode::Shared{}; - shadowTreeRegistry_.visit( - shadowNode.getSurfaceId(), [&](const ShadowTree& shadowTree) { - ancestorShadowNode = shadowTree.getCurrentRevision().rootShadowNode; - }); - - if (!ancestorShadowNode) { - return nullptr; - } - - auto ancestors = shadowNode.getFamily().getAncestors(*ancestorShadowNode); - - if (ancestors.empty()) { - return nullptr; - } - - if (ancestors.size() == 1) { - // The parent is the shadow root - return ancestorShadowNode; - } - - auto parentOfParentPair = ancestors[ancestors.size() - 2]; - return parentOfParentPair.first.get().getChildren().at( - parentOfParentPair.second); -} - -ShadowNode::Shared UIManager::getNewestPositionedAncestorOfShadowNode( - const ShadowNode& shadowNode) const { - auto rootShadowNode = ShadowNode::Shared{}; - shadowTreeRegistry_.visit( - shadowNode.getSurfaceId(), [&](const ShadowTree& shadowTree) { - rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode; - }); - - if (!rootShadowNode) { - return nullptr; - } - - auto ancestors = shadowNode.getFamily().getAncestors(*rootShadowNode); - - if (ancestors.empty()) { - return nullptr; - } - - for (auto it = ancestors.rbegin(); it != ancestors.rend(); it++) { - const auto layoutableAncestorShadowNode = - dynamic_cast(&(it->first.get())); - if (layoutableAncestorShadowNode == nullptr) { - return nullptr; - } - if (layoutableAncestorShadowNode->getLayoutMetrics().positionType != - PositionType::Static) { - // We have found our nearest positioned ancestor, now to get a shared - // pointer of it - it++; - if (it != ancestors.rend()) { - return it->first.get().getChildren().at(it->second); - } - // else the positioned ancestor is the root which we return outside of the - // loop - } - } - - // If there is no positioned ancestor then we just consider the root - // to be one - return rootShadowNode; -} - -std::string UIManager::getTextContentInNewestCloneOfShadowNode( - const ShadowNode& shadowNode) const { - auto newestCloneOfShadowNode = getNewestCloneOfShadowNode(shadowNode); - std::string result; - getTextContentInShadowNode(*newestCloneOfShadowNode, result); - return result; -} - -int UIManager::compareDocumentPosition( - const ShadowNode& shadowNode, - const ShadowNode& otherShadowNode) const { - // Quick check for node vs. itself - if (&shadowNode == &otherShadowNode) { - return 0; - } - - if (shadowNode.getSurfaceId() != otherShadowNode.getSurfaceId()) { - return DOCUMENT_POSITION_DISCONNECTED; - } - - auto ancestorShadowNode = ShadowNode::Shared{}; - shadowTreeRegistry_.visit( - shadowNode.getSurfaceId(), [&](const ShadowTree& shadowTree) { - ancestorShadowNode = shadowTree.getCurrentRevision().rootShadowNode; - }); - if (!ancestorShadowNode) { - return DOCUMENT_POSITION_DISCONNECTED; - } - - auto ancestors = shadowNode.getFamily().getAncestors(*ancestorShadowNode); - if (ancestors.empty()) { - return DOCUMENT_POSITION_DISCONNECTED; - } - - auto otherAncestors = - otherShadowNode.getFamily().getAncestors(*ancestorShadowNode); - if (ancestors.empty()) { - return DOCUMENT_POSITION_DISCONNECTED; - } - - // Consume all common ancestors - size_t i = 0; - while (i < ancestors.size() && i < otherAncestors.size() && - ancestors[i].second == otherAncestors[i].second) { - i++; - } - - if (i == ancestors.size()) { - return (DOCUMENT_POSITION_CONTAINED_BY | DOCUMENT_POSITION_FOLLOWING); - } - - if (i == otherAncestors.size()) { - return (DOCUMENT_POSITION_CONTAINS | DOCUMENT_POSITION_PRECEDING); - } - - if (ancestors[i].second > otherAncestors[i].second) { - return DOCUMENT_POSITION_PRECEDING; - } - - return DOCUMENT_POSITION_FOLLOWING; -} - ShadowTreeRevisionConsistencyManager* UIManager::getShadowTreeRevisionConsistencyManager() { return lazyShadowTreeRevisionConsistencyManager_.get(); diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index 936b963a4cee..d32dca94b190 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -97,19 +97,6 @@ class UIManager final : public ShadowTreeDelegate { ShadowNode::Shared getNewestCloneOfShadowNode( const ShadowNode& shadowNode) const; - ShadowNode::Shared getNewestParentOfShadowNode( - const ShadowNode& shadowNode) const; - - ShadowNode::Shared getNewestPositionedAncestorOfShadowNode( - const ShadowNode& shadowNode) const; - - std::string getTextContentInNewestCloneOfShadowNode( - const ShadowNode& shadowNode) const; - - int compareDocumentPosition( - const ShadowNode& shadowNode, - const ShadowNode& otherShadowNode) const; - ShadowTreeRevisionConsistencyManager* getShadowTreeRevisionConsistencyManager(); ShadowTreeRevisionProvider* getShadowTreeRevisionProvider(); diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 2f9e26e0868b..9e674960d1fe 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -868,25 +869,32 @@ jsi::Value UIManagerBinding::get( size_t count) -> jsi::Value { validateArgumentCount(runtime, methodName, paramCount, count); + auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); bool includeTransform = arguments[1].getBool(); - auto layoutMetrics = uiManager->getRelativeLayoutMetrics( - *shadowNodeFromValue(runtime, arguments[0]), - nullptr, - {/* .includeTransform = */ includeTransform, - /* .includeViewportOffset = */ true}); + auto currentRevision = + uiManager->getShadowTreeRevisionProvider()->getCurrentRevision( + shadowNode->getSurfaceId()); - if (layoutMetrics == EmptyLayoutMetrics) { + if (currentRevision == nullptr) { return jsi::Value::undefined(); } - auto frame = layoutMetrics.frame; + auto result = dom::getBoundingClientRect( + currentRevision, *shadowNode, includeTransform); + + if (!result) { + return jsi::Value::undefined(); + } + + auto [x, y, width, height] = result.value(); + return jsi::Array::createWithElements( runtime, - jsi::Value{runtime, (double)frame.origin.x}, - jsi::Value{runtime, (double)frame.origin.y}, - jsi::Value{runtime, (double)frame.size.width}, - jsi::Value{runtime, (double)frame.size.height}); + jsi::Value{runtime, x}, + jsi::Value{runtime, y}, + jsi::Value{runtime, width}, + jsi::Value{runtime, height}); }); } @@ -909,10 +917,18 @@ jsi::Value UIManagerBinding::get( auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); auto otherShadowNode = shadowNodeFromValue(runtime, arguments[1]); - auto documentPosition = - uiManager->compareDocumentPosition(*shadowNode, *otherShadowNode); + auto currentRevision = + uiManager->getShadowTreeRevisionProvider()->getCurrentRevision( + shadowNode->getSurfaceId()); + + double documentPosition = 0; + + if (currentRevision != nullptr) { + documentPosition = (double)dom::compareDocumentPosition( + currentRevision, *shadowNode, *otherShadowNode); + } - return jsi::Value(documentPosition); + return {documentPosition}; }); } diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h b/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h index 7984b902bc27..03552d2b140f 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/primitives.h @@ -11,10 +11,7 @@ #include #include #include -#include -#include #include -#include namespace facebook::react { @@ -166,70 +163,4 @@ inline static folly::dynamic commandArgsFromValue( const jsi::Value& value) { return jsi::dynamicFromValue(runtime, value); } - -inline static std::vector getArrayOfInstanceHandlesFromShadowNodes( - const ShadowNode::ListOfShared& nodes, - jsi::Runtime& runtime) { - // JSI doesn't support adding elements to an array after creation, - // so we need to accumulate the values in a vector and then create - // the array when we know the size. - std::vector nonNullInstanceHandles; - nonNullInstanceHandles.reserve(nodes.size()); - for (const auto& shadowNode : nodes) { - auto instanceHandle = (*shadowNode).getInstanceHandle(runtime); - if (!instanceHandle.isNull()) { - nonNullInstanceHandles.push_back(std::move(instanceHandle)); - } - } - - return nonNullInstanceHandles; -} - -inline static void getTextContentInShadowNode( - const ShadowNode& shadowNode, - std::string& result) { - auto rawTextShadowNode = dynamic_cast(&shadowNode); - if (rawTextShadowNode != nullptr) { - result.append(rawTextShadowNode->getConcreteProps().text); - } - - for (const auto& childNode : shadowNode.getChildren()) { - getTextContentInShadowNode(*childNode.get(), result); - } -} - -inline static Rect getScrollableContentBounds( - Rect contentBounds, - LayoutMetrics layoutMetrics) { - auto paddingFrame = layoutMetrics.getPaddingFrame(); - - auto paddingBottom = - layoutMetrics.contentInsets.bottom - layoutMetrics.borderWidth.bottom; - auto paddingLeft = - layoutMetrics.contentInsets.left - layoutMetrics.borderWidth.left; - auto paddingRight = - layoutMetrics.contentInsets.right - layoutMetrics.borderWidth.right; - - auto minY = paddingFrame.getMinY(); - auto maxY = - std::max(paddingFrame.getMaxY(), contentBounds.getMaxY() + paddingBottom); - - auto minX = layoutMetrics.layoutDirection == LayoutDirection::RightToLeft - ? std::min(paddingFrame.getMinX(), contentBounds.getMinX() - paddingLeft) - : paddingFrame.getMinX(); - auto maxX = layoutMetrics.layoutDirection == LayoutDirection::RightToLeft - ? paddingFrame.getMaxX() - : std::max( - paddingFrame.getMaxX(), contentBounds.getMaxX() + paddingRight); - - return Rect{Point{minX, minY}, Size{maxX - minX, maxY - minY}}; -} - -inline static Size getScrollSize( - LayoutMetrics layoutMetrics, - Rect contentBounds) { - auto scrollableContentBounds = - getScrollableContentBounds(contentBounds, layoutMetrics); - return scrollableContentBounds.size; -} } // namespace facebook::react From 938670c85ce37bde579ff5982cecc1f8ef00249f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Mon, 25 Mar 2024 05:06:51 -0700 Subject: [PATCH 3/3] Implement UI consistency in legacy RN layout APIs (#43579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/43579 Changelog: [internal] This updates all the legacy layout APIs in React Native to make use of the UI consistency mechanism introduced in Fabric (if available, otherwise the behavior is the same we have now — we consume the latest version of the tree available). Reviewed By: sammy-SC Differential Revision: D55077309 --- .../ReactCommon/react/renderer/dom/DOM.cpp | 114 +++++++++++++++++ .../ReactCommon/react/renderer/dom/DOM.h | 32 +++++ .../renderer/uimanager/UIManagerBinding.cpp | 115 ++++++++++-------- 3 files changed, 212 insertions(+), 49 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp b/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp index f52101b7da7c..89405542e1e0 100644 --- a/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp +++ b/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp @@ -486,4 +486,118 @@ std::string getTagName(const ShadowNode& shadowNode) { return canonicalComponentName; } +std::optional> +measure( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + auto layoutMetrics = getRelativeLayoutMetrics( + *currentRevision, + *shadowNodeInCurrentRevision, + {.includeTransform = true, .includeViewportOffset = false}); + + if (layoutMetrics == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto layoutableShadowNode = dynamic_cast( + shadowNodeInCurrentRevision.get()); + Point originRelativeToParent = layoutableShadowNode != nullptr + ? layoutableShadowNode->getLayoutMetrics().frame.origin + : Point(); + + auto frame = layoutMetrics.frame; + + return std::tuple{ + (double)originRelativeToParent.x, + (double)originRelativeToParent.y, + (double)frame.size.width, + (double)frame.size.height, + (double)frame.origin.x, + (double)frame.origin.y}; +} + +std::optional> +measureInWindow( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + auto layoutMetrics = getRelativeLayoutMetrics( + *currentRevision, + *shadowNodeInCurrentRevision, + {.includeTransform = true, .includeViewportOffset = true}); + + if (layoutMetrics == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto frame = layoutMetrics.frame; + return std::tuple{ + (double)frame.origin.x, + (double)frame.origin.y, + (double)frame.size.width, + (double)frame.size.height, + }; +} + +std::optional> +measureLayout( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode, + const ShadowNode& relativeToShadowNode) { + auto shadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, shadowNode); + if (shadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + auto relativeToShadowNodeInCurrentRevision = + getShadowNodeInRevision(currentRevision, relativeToShadowNode); + if (relativeToShadowNodeInCurrentRevision == nullptr) { + return std::nullopt; + } + + auto layoutMetrics = getRelativeLayoutMetrics( + *relativeToShadowNodeInCurrentRevision, + *shadowNodeInCurrentRevision, + {.includeTransform = false}); + + if (layoutMetrics == EmptyLayoutMetrics) { + return std::nullopt; + } + + auto frame = layoutMetrics.frame; + + return std::tuple{ + (double)frame.origin.x, + (double)frame.origin.y, + (double)frame.size.width, + (double)frame.size.height, + }; +} + } // namespace facebook::react::dom diff --git a/packages/react-native/ReactCommon/react/renderer/dom/DOM.h b/packages/react-native/ReactCommon/react/renderer/dom/DOM.h index a922c514842e..9952d6d5b191 100644 --- a/packages/react-native/ReactCommon/react/renderer/dom/DOM.h +++ b/packages/react-native/ReactCommon/react/renderer/dom/DOM.h @@ -84,4 +84,36 @@ getBorderSize( std::string getTagName(const ShadowNode& shadowNode); +// Non-standard methods from React Native + +std::optional> +measure( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> +measureInWindow( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode); + +std::optional> +measureLayout( + const RootShadowNode::Shared& currentRevision, + const ShadowNode& shadowNode, + const ShadowNode& relativeToShadowNode); + } // namespace facebook::react::dom diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 9e674960d1fe..7b6b038c3850 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -654,28 +654,38 @@ jsi::Value UIManagerBinding::get( size_t count) { validateArgumentCount(runtime, methodName, paramCount, count); - auto layoutMetrics = uiManager->getRelativeLayoutMetrics( - *shadowNodeFromValue(runtime, arguments[0]), - shadowNodeFromValue(runtime, arguments[1]).get(), - {/* .includeTransform = */ false}); + auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); + auto relativeToShadowNode = + shadowNodeFromValue(runtime, arguments[1]); + auto onFailFunction = + arguments[2].getObject(runtime).getFunction(runtime); + auto onSuccessFunction = + arguments[3].getObject(runtime).getFunction(runtime); - if (layoutMetrics == EmptyLayoutMetrics) { - auto onFailFunction = - arguments[2].getObject(runtime).getFunction(runtime); + auto currentRevision = + uiManager->getShadowTreeRevisionProvider()->getCurrentRevision( + shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { onFailFunction.call(runtime); return jsi::Value::undefined(); } - auto onSuccessFunction = - arguments[3].getObject(runtime).getFunction(runtime); - auto frame = layoutMetrics.frame; + auto result = dom::measureLayout( + currentRevision, *shadowNode, *relativeToShadowNode); + + if (!result) { + onFailFunction.call(runtime); + return jsi::Value::undefined(); + } + + auto [x, y, width, height] = result.value(); onSuccessFunction.call( runtime, - {jsi::Value{runtime, (double)frame.origin.x}, - jsi::Value{runtime, (double)frame.origin.y}, - jsi::Value{runtime, (double)frame.size.width}, - jsi::Value{runtime, (double)frame.size.height}}); + {jsi::Value{runtime, x}, + jsi::Value{runtime, y}, + jsi::Value{runtime, width}, + jsi::Value{runtime, height}}); return jsi::Value::undefined(); }); } @@ -694,33 +704,34 @@ jsi::Value UIManagerBinding::get( validateArgumentCount(runtime, methodName, paramCount, count); auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); - auto layoutMetrics = uiManager->getRelativeLayoutMetrics( - *shadowNode, nullptr, {/* .includeTransform = */ true}); - auto onSuccessFunction = + auto callbackFunction = arguments[1].getObject(runtime).getFunction(runtime); - if (layoutMetrics == EmptyLayoutMetrics) { - onSuccessFunction.call(runtime, {0, 0, 0, 0, 0, 0}); + auto currentRevision = + uiManager->getShadowTreeRevisionProvider()->getCurrentRevision( + shadowNode->getSurfaceId()); + if (currentRevision == nullptr) { + callbackFunction.call(runtime, {0, 0, 0, 0, 0, 0}); return jsi::Value::undefined(); } - auto newestCloneOfShadowNode = - uiManager->getNewestCloneOfShadowNode(*shadowNode); - auto layoutableShadowNode = dynamic_cast( - newestCloneOfShadowNode.get()); - Point originRelativeToParent = layoutableShadowNode != nullptr - ? layoutableShadowNode->getLayoutMetrics().frame.origin - : Point(); + auto result = dom::measure(currentRevision, *shadowNode); - auto frame = layoutMetrics.frame; - onSuccessFunction.call( + if (!result) { + callbackFunction.call(runtime, {0, 0, 0, 0, 0, 0}); + return jsi::Value::undefined(); + } + + auto [x, y, width, height, pageX, pageY] = result.value(); + + callbackFunction.call( runtime, - {jsi::Value{runtime, (double)originRelativeToParent.x}, - jsi::Value{runtime, (double)originRelativeToParent.y}, - jsi::Value{runtime, (double)frame.size.width}, - jsi::Value{runtime, (double)frame.size.height}, - jsi::Value{runtime, (double)frame.origin.x}, - jsi::Value{runtime, (double)frame.origin.y}}); + {jsi::Value{runtime, x}, + jsi::Value{runtime, y}, + jsi::Value{runtime, width}, + jsi::Value{runtime, height}, + jsi::Value{runtime, pageX}, + jsi::Value{runtime, pageY}}); return jsi::Value::undefined(); }); } @@ -738,27 +749,33 @@ jsi::Value UIManagerBinding::get( size_t count) { validateArgumentCount(runtime, methodName, paramCount, count); - auto layoutMetrics = uiManager->getRelativeLayoutMetrics( - *shadowNodeFromValue(runtime, arguments[0]), - nullptr, - {/* .includeTransform = */ true, - /* .includeViewportOffset = */ true}); - - auto onSuccessFunction = + auto shadowNode = shadowNodeFromValue(runtime, arguments[0]); + auto callbackFunction = arguments[1].getObject(runtime).getFunction(runtime); - if (layoutMetrics == EmptyLayoutMetrics) { - onSuccessFunction.call(runtime, {0, 0, 0, 0}); + auto currentRevision = + uiManager->getShadowTreeRevisionProvider()->getCurrentRevision( + shadowNode->getSurfaceId()); + + if (currentRevision == nullptr) { + callbackFunction.call(runtime, {0, 0, 0, 0, 0, 0}); return jsi::Value::undefined(); } - auto frame = layoutMetrics.frame; - onSuccessFunction.call( + auto result = dom::measureInWindow(currentRevision, *shadowNode); + if (!result) { + callbackFunction.call(runtime, {0, 0, 0, 0, 0, 0}); + return jsi::Value::undefined(); + } + + auto [x, y, width, height] = result.value(); + + callbackFunction.call( runtime, - {jsi::Value{runtime, (double)frame.origin.x}, - jsi::Value{runtime, (double)frame.origin.y}, - jsi::Value{runtime, (double)frame.size.width}, - jsi::Value{runtime, (double)frame.size.height}}); + {jsi::Value{runtime, x}, + jsi::Value{runtime, y}, + jsi::Value{runtime, width}, + jsi::Value{runtime, height}}); return jsi::Value::undefined(); }); }