diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index d8ee6cb3d8b0bb..fde373ba822035 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -437,7 +437,7 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view - (void)scrollViewDidScroll:(UIScrollView *)scrollView { - if (!_isUserTriggeredScrolling) { + if (!_isUserTriggeredScrolling || CoreFeatures::enableGranularScrollViewStateUpdatesIOS) { [self _updateStateWithContentOffset]; } diff --git a/packages/react-native/React/Fabric/RCTScheduler.h b/packages/react-native/React/Fabric/RCTScheduler.h index 1424fc6c4bd863..e84a1268bb84c2 100644 --- a/packages/react-native/React/Fabric/RCTScheduler.h +++ b/packages/react-native/React/Fabric/RCTScheduler.h @@ -67,6 +67,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)animationTick; +- (void)reportMount:(facebook::react::SurfaceId)surfaceId; + - (void)addEventListener:(std::shared_ptr const &)listener; - (void)removeEventListener:(std::shared_ptr const &)listener; diff --git a/packages/react-native/React/Fabric/RCTScheduler.mm b/packages/react-native/React/Fabric/RCTScheduler.mm index 1e4641c7c30e6f..5c21d443c681f8 100644 --- a/packages/react-native/React/Fabric/RCTScheduler.mm +++ b/packages/react-native/React/Fabric/RCTScheduler.mm @@ -132,6 +132,11 @@ - (void)animationTick _scheduler->animationTick(); } +- (void)reportMount:(facebook::react::SurfaceId)surfaceId +{ + _scheduler->reportMount(surfaceId); +} + - (void)dealloc { if (_animationDriver) { diff --git a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm index 7b09c995700cca..3e11f245198c48 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm @@ -276,6 +276,14 @@ - (RCTScheduler *)_createScheduler CoreFeatures::disableTransactionCommit = true; } + if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:enable_granular_scroll_view_state_updates_ios")) { + CoreFeatures::enableGranularScrollViewStateUpdatesIOS = true; + } + + if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:enable_mount_hooks_ios")) { + CoreFeatures::enableMountHooks = true; + } + auto componentRegistryFactory = [factory = wrapManagedObject(_mountingManager.componentViewRegistry.componentViewFactory)]( EventDispatcher::Weak const &eventDispatcher, ContextContainer::Shared const &contextContainer) { @@ -438,6 +446,15 @@ - (void)mountingManager:(RCTMountingManager *)mountingManager didMountComponents [observer didMountComponentsWithRootTag:rootTag]; } } + + RCTScheduler *scheduler = [self scheduler]; + if (scheduler) { + // Notify mount when the effects are visible and prevent mount hooks to + // delay paint. + dispatch_async(dispatch_get_main_queue(), ^{ + [scheduler reportMount:rootTag]; + }); + } } - (NSArray> *)_getObservers diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index ef908cc099778b..6eb5b53b12f552 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -156,4 +156,7 @@ public class ReactFeatureFlags { * HostObject pattern */ public static boolean useNativeState = false; + + /** Report mount operations from the host platform to notify mount hooks. */ + public static boolean enableMountHooks = false; } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/Binding.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/Binding.java index 26a2949483ad48..e99632b25bed77 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/Binding.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/Binding.java @@ -52,6 +52,8 @@ public void setConstraints( public void driveCxxAnimations(); + public void reportMount(int surfaceId); + public ReadableNativeMap getInspectorDataForInstance(EventEmitterWrapper eventEmitterWrapper); public void register( diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/BindingImpl.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/BindingImpl.java index 352f051c040321..a05dc0ef207bed 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/BindingImpl.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/BindingImpl.java @@ -86,6 +86,8 @@ public native void setConstraints( public native void driveCxxAnimations(); + public native void reportMount(int surfaceId); + public native ReadableNativeMap getInspectorDataForInstance( EventEmitterWrapper eventEmitterWrapper); diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index f9057468f37dc7..5f36d0e23a30f6 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -22,6 +22,8 @@ import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Point; +import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; import android.view.View; import android.view.accessibility.AccessibilityEvent; @@ -81,10 +83,13 @@ import com.facebook.react.uimanager.events.RCTEventEmitter; import com.facebook.react.views.text.TextLayoutManager; import com.facebook.react.views.text.TextLayoutManagerMapBuffer; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; /** * We instruct ProGuard not to strip out any fields or methods, because many of these methods are @@ -166,6 +171,9 @@ public class FabricUIManager implements UIManager, LifecycleEventListener { @NonNull private final CopyOnWriteArrayList mListeners = new CopyOnWriteArrayList<>(); + @NonNull private final AtomicBoolean mMountNotificationScheduled = new AtomicBoolean(false); + @NonNull private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); + @ThreadConfined(UI) @NonNull private final DispatchUIFrameCallback mDispatchUIFrameCallback; @@ -1179,17 +1187,50 @@ public Map getPerformanceCounters() { private class MountItemDispatchListener implements MountItemDispatcher.ItemDispatchListener { @Override - public void willMountItems() { + public void willMountItems(@Nullable List mountItems) { for (UIManagerListener listener : mListeners) { listener.willMountItems(FabricUIManager.this); } } @Override - public void didMountItems() { + public void didMountItems(@Nullable List mountItems) { for (UIManagerListener listener : mListeners) { listener.didMountItems(FabricUIManager.this); } + + if (!ReactFeatureFlags.enableMountHooks) { + return; + } + + boolean mountNotificationScheduled = mMountNotificationScheduled.getAndSet(true); + if (!mountNotificationScheduled) { + // Notify mount when the effects are visible and prevent mount hooks to + // delay paint. + mMainThreadHandler.postAtFrontOfQueue( + new Runnable() { + @Override + public void run() { + mMountNotificationScheduled.set(false); + + if (mountItems == null) { + return; + } + + // Collect surface IDs for all the mount items + List surfaceIds = new ArrayList(); + for (MountItem mountItem : mountItems) { + if (!surfaceIds.contains(mountItem.getSurfaceId())) { + surfaceIds.add(mountItem.getSurfaceId()); + } + } + + for (int surfaceId : surfaceIds) { + mBinding.reportMount(surfaceId); + } + } + }); + } } @Override diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountItemDispatcher.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountItemDispatcher.java index 6861961c472640..8b8ae700fa4677 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountItemDispatcher.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountItemDispatcher.java @@ -192,7 +192,7 @@ private boolean dispatchMountItems() { return false; } - mItemDispatchListener.willMountItems(); + mItemDispatchListener.willMountItems(mountItemsToDispatch); // As an optimization, execute all ViewCommands first // This should be: @@ -301,7 +301,7 @@ private boolean dispatchMountItems() { mBatchedExecutionTime += SystemClock.uptimeMillis() - batchedExecutionStartTime; } - mItemDispatchListener.didMountItems(); + mItemDispatchListener.didMountItems(mountItemsToDispatch); Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); @@ -419,9 +419,9 @@ private static void printMountItem(MountItem mountItem, String prefix) { } public interface ItemDispatchListener { - void willMountItems(); + void willMountItems(List mountItems); - void didMountItems(); + void didMountItems(List mountItems); void didDispatchMountItems(); } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp index 26e838f1a1f72d..713f8c8c18381f 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp @@ -97,6 +97,15 @@ void Binding::driveCxxAnimations() { scheduler_->animationTick(); } +void Binding::reportMount(SurfaceId surfaceId) { + const auto &scheduler = getScheduler(); + if (!scheduler) { + LOG(ERROR) << "Binding::reportMount: scheduler disappeared"; + return; + } + scheduler->reportMount(surfaceId); +} + #pragma mark - Surface management void Binding::startSurface( @@ -570,6 +579,7 @@ void Binding::registerNatives() { makeNativeMethod("setConstraints", Binding::setConstraints), makeNativeMethod("setPixelDensity", Binding::setPixelDensity), makeNativeMethod("driveCxxAnimations", Binding::driveCxxAnimations), + makeNativeMethod("reportMount", Binding::reportMount), makeNativeMethod( "uninstallFabricUIManager", Binding::uninstallFabricUIManager), makeNativeMethod("registerSurface", Binding::registerSurface), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.h index cee8ba004d3ee2..b6e1cca5dd83ab 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.h @@ -119,6 +119,7 @@ class Binding : public jni::HybridClass, void setPixelDensity(float pointScaleFactor); void driveCxxAnimations(); + void reportMount(SurfaceId surfaceId); void uninstallFabricUIManager(); diff --git a/packages/react-native/ReactCommon/React-Fabric.podspec b/packages/react-native/ReactCommon/React-Fabric.podspec index 2ab06bc8f00619..1347d9dad3e03a 100644 --- a/packages/react-native/ReactCommon/React-Fabric.podspec +++ b/packages/react-native/ReactCommon/React-Fabric.podspec @@ -54,6 +54,7 @@ Pod::Spec.new do |s| s.dependency "React-debug" s.dependency "React-utils" s.dependency "React-runtimescheduler" + s.dependency "React-cxxreact" if ENV["USE_HERMES"] == nil || ENV["USE_HERMES"] == "1" s.dependency "hermes-engine" diff --git a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp index 351a257d67877e..a15227df1d06af 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp @@ -16,5 +16,7 @@ bool CoreFeatures::useNativeState = false; bool CoreFeatures::cacheLastTextMeasurement = false; bool CoreFeatures::cancelImageDownloadsOnRecycle = false; bool CoreFeatures::disableTransactionCommit = false; +bool CoreFeatures::enableGranularScrollViewStateUpdatesIOS = false; +bool CoreFeatures::enableMountHooks = false; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h index f9a07875d270f2..9c6cb00ba93eb6 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h +++ b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h @@ -48,6 +48,13 @@ class CoreFeatures { // [CATransaction end] This feature flag disables it to measure its impact in // production. static bool disableTransactionCommit; + + // When enabled, RCTScrollViewComponentView will trigger ShadowTree state + // updates for all changes in scroll position. + static bool enableGranularScrollViewStateUpdatesIOS; + + // Report mount operations from the host platform to notify mount hooks. + static bool enableMountHooks; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h index f28ba3322fb799..16faa8bc78c6e2 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h @@ -20,17 +20,23 @@ namespace facebook::react { * Describes results of layout process for particular shadow node. */ struct LayoutMetrics { - // Origin: relative to its parent content frame (unless using a method that - // computes it relative to other parent or the viewport) + // Origin: relative to the outer border of its parent. // Size: includes border, padding and content. Rect frame; - // Width of the border + padding in all directions. + // Width of the border + padding in each direction. EdgeInsets contentInsets{0}; - // Width of the border in all directions. + // Width of the border in each direction. EdgeInsets borderWidth{0}; + // See `DisplayType` for all possible options. DisplayType displayType{DisplayType::Flex}; + // See `LayoutDirection` for all possible options. LayoutDirection layoutDirection{LayoutDirection::Undefined}; + // Pixel density. Number of device pixels per density-independent pixel. Float pointScaleFactor{1.0}; + // How much the children of the node actually overflow in each direction. + // Positive values indicate that children are overflowing outside of the node. + // Negative values indicate that children are clipped inside the node + // (like when using `overflow: clip` on Web). EdgeInsets overflowInset{}; // Origin: the outer border of the node. diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp index 3bc848f468512a..41b31772b18cd1 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp @@ -52,14 +52,14 @@ static LayoutableSmallVector calculateTransformedFrames( if (i != size) { auto parentShadowNode = traitCast(shadowNodeList.at(i)); - auto contentOritinOffset = parentShadowNode->getContentOriginOffset(); + auto contentOriginOffset = parentShadowNode->getContentOriginOffset(); if (Transform::isVerticalInversion(transformation)) { - contentOritinOffset.y = -contentOritinOffset.y; + contentOriginOffset.y = -contentOriginOffset.y; } if (Transform::isHorizontalInversion(transformation)) { - contentOritinOffset.x = -contentOritinOffset.x; + contentOriginOffset.x = -contentOriginOffset.x; } - currentFrame.origin += contentOritinOffset; + currentFrame.origin += contentOriginOffset; } transformation = transformation * currentShadowNode->getTransform(); @@ -105,7 +105,12 @@ LayoutMetrics LayoutableShadowNode::computeRelativeLayoutMetrics( } auto ancestors = descendantNodeFamily.getAncestors(ancestorNode); + return computeRelativeLayoutMetrics(ancestors, policy); +} +LayoutMetrics LayoutableShadowNode::computeRelativeLayoutMetrics( + AncestorList const &ancestors, + LayoutInspectingPolicy policy) { if (ancestors.empty()) { // Specified nodes do not form an ancestor-descender relationship // in the same tree. Aborting. @@ -174,7 +179,7 @@ LayoutMetrics LayoutableShadowNode::computeRelativeLayoutMetrics( resultFrame.origin = {0, 0}; // Step 3. - // Iterating on a list of nodes computing compound offset. + // Iterating on a list of nodes computing compound offset and size. auto size = shadowNodeList.size(); for (size_t i = 0; i < size; i++) { auto currentShadowNode = @@ -200,19 +205,35 @@ LayoutMetrics LayoutableShadowNode::computeRelativeLayoutMetrics( auto isRootNode = currentShadowNode->getTraits().check( ShadowNodeTraits::Trait::RootNodeKind); + auto shouldApplyTransformation = (policy.includeTransform && !isRootNode) || (policy.includeViewportOffset && isRootNode); + // Move frame to the coordinate space of the current node. + resultFrame.origin += currentFrame.origin; + if (shouldApplyTransformation) { - resultFrame.size = resultFrame.size * currentShadowNode->getTransform(); - currentFrame = currentFrame * currentShadowNode->getTransform(); + // If a node has a transform, we need to use the center of that node as + // the origin of the transform when transforming its children (which + // affects the result of transforms like `scale` and `rotate`). + resultFrame = currentShadowNode->getTransform().applyWithCenter( + resultFrame, currentFrame.getCenter()); } - resultFrame.origin += currentFrame.origin; if (!shouldCalculateTransformedFrames && i != 0 && policy.includeTransform) { resultFrame.origin += currentShadowNode->getContentOriginOffset(); } + + if (policy.enableOverflowClipping) { + auto overflowInset = currentShadowNode->getLayoutMetrics().overflowInset; + auto overflowRect = insetBy( + currentFrame * currentShadowNode->getTransform(), overflowInset); + resultFrame = Rect::intersect(resultFrame, overflowRect); + if (resultFrame.size.width == 0 && resultFrame.size.height == 0) { + return EmptyLayoutMetrics; + } + } } // ------------------------------ diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h index b16213ad8f830b..ed0dc70088697b 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h @@ -46,6 +46,7 @@ class LayoutableShadowNode : public ShadowNode { struct LayoutInspectingPolicy { bool includeTransform{true}; bool includeViewportOffset{false}; + bool enableOverflowClipping{false}; }; using UnsharedList = butter:: @@ -62,6 +63,13 @@ class LayoutableShadowNode : public ShadowNode { LayoutableShadowNode const &ancestorNode, LayoutInspectingPolicy policy); + /* + * Computes the layout metrics of a node relative to its specified ancestors. + */ + static LayoutMetrics computeRelativeLayoutMetrics( + AncestorList const &ancestors, + LayoutInspectingPolicy policy); + /* * Performs layout of the tree starting from this node. Usually is being * called on the root node. diff --git a/packages/react-native/ReactCommon/react/renderer/core/tests/LayoutableShadowNodeTest.cpp b/packages/react-native/ReactCommon/react/renderer/core/tests/LayoutableShadowNodeTest.cpp index 9be2fae835cc64..1dd21542502eb8 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/tests/LayoutableShadowNodeTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/tests/LayoutableShadowNodeTest.cpp @@ -377,13 +377,156 @@ TEST(LayoutableShadowNodeTest, relativeLayoutMetricsOnTransformedParent) { LayoutableShadowNode::computeRelativeLayoutMetrics( childShadowNode->getFamily(), *parentShadowNode, {}); - EXPECT_EQ(relativeLayoutMetrics.frame.origin.x, 45); - EXPECT_EQ(relativeLayoutMetrics.frame.origin.y, 45); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.x, 40); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.y, 40); EXPECT_EQ(relativeLayoutMetrics.frame.size.width, 25); EXPECT_EQ(relativeLayoutMetrics.frame.size.height, 25); } +/* + * ┌────────────────────────┐ + * │ │ + * │ ┌─────────────────────┐│ + * │ │ ││ + * │ │ ┌──────────────┐││ + * │ │ │ │││ + * │ │ │ ┌──────────┐│││ + * │ │ │ │ ││││ + * │ │ │ │ ││││ + * │ │ │ │ ││││ + * │ │ │ └──────────┘│││ + * │ │ └──────────────┘││ + * │ └─────────────────────┘│ + * └────────────────────────┘ + */ +TEST(LayoutableShadowNodeTest, relativeLayoutMetricsOnParentWithClipping) { + auto builder = simpleComponentBuilder(); + + auto childShadowNode = std::shared_ptr{}; + // clang-format off + auto element = + Element() + .finalize([](RootShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {900, 900}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {10, 10}; + layoutMetrics.frame.size = {100, 100}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .reference(childShadowNode) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {10, 10}; + layoutMetrics.frame.size = {150, 150}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + }) + }); + // clang-format on + + auto parentShadowNode = builder.build(element); + + auto relativeLayoutMetrics = + LayoutableShadowNode::computeRelativeLayoutMetrics( + childShadowNode->getFamily(), + *parentShadowNode, + { + /* includeTransform = */ true, + /* includeViewportOffset = */ false, + /* enableOverflowClipping = */ true, + }); + + EXPECT_EQ(relativeLayoutMetrics.frame.origin.x, 20); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.y, 20); + + EXPECT_EQ(relativeLayoutMetrics.frame.size.width, 90); + EXPECT_EQ(relativeLayoutMetrics.frame.size.height, 90); +} + +/* + * ┌────────────────────────┐ + * │ │ + * │ ┌─────────────────────┐│ + * │ │ ││ + * │ │ ┌──────────────┐││ + * │ │ │ │││ + * │ │ │ ┌──────────┐│││ + * │ │ │ │ ││││ + * │ │ │ │ ││││ + * │ │ │ │ ││││ + * │ │ │ └──────────┘│││ + * │ │ └──────────────┘││ + * │ └─────────────────────┘│ + * └────────────────────────┘ + */ +TEST( + LayoutableShadowNodeTest, + relativeLayoutMetricsOnTransformedParentWithClipping) { + auto builder = simpleComponentBuilder(); + + auto childShadowNode = std::shared_ptr{}; + // clang-format off + auto element = + Element() + .finalize([](RootShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {900, 900}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .props([] { + auto sharedProps = std::make_shared(); + sharedProps->transform = Transform::Scale(0.5, 0.5, 1); + return sharedProps; + }) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {10, 10}; + layoutMetrics.frame.size = {100, 100}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .reference(childShadowNode) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {10, 10}; + layoutMetrics.frame.size = {150, 150}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + }) + }); + // clang-format on + + auto parentShadowNode = builder.build(element); + + auto relativeLayoutMetrics = + LayoutableShadowNode::computeRelativeLayoutMetrics( + childShadowNode->getFamily(), + *parentShadowNode, + { + /* includeTransform = */ true, + /* includeViewportOffset = */ false, + /* enableOverflowClipping = */ true, + }); + + EXPECT_EQ(relativeLayoutMetrics.frame.origin.x, 40); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.y, 40); + + EXPECT_EQ(relativeLayoutMetrics.frame.size.width, 45); + EXPECT_EQ(relativeLayoutMetrics.frame.size.height, 45); +} + /* * ┌────────────────┐ * │ │ diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Rect.h b/packages/react-native/ReactCommon/react/renderer/graphics/Rect.h index 6c549ac02db69f..321b21472c9d47 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/Rect.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Rect.h @@ -69,6 +69,24 @@ struct Rect { point.y <= (origin.y + size.height); } + static Rect intersect(Rect const &rect1, Rect const &rect2) { + Float x1 = std::max(rect1.origin.x, rect2.origin.x); + Float y1 = std::max(rect1.origin.y, rect2.origin.y); + Float x2 = std::min( + rect1.origin.x + rect1.size.width, rect2.origin.x + rect2.size.width); + Float y2 = std::min( + rect1.origin.y + rect1.size.height, rect2.origin.y + rect2.size.height); + + Float intersectionWidth = x2 - x1; + Float intersectionHeight = y2 - y1; + + if (intersectionWidth < 0 || intersectionHeight < 0) { + return {}; + } + + return {{x1, y1}, {intersectionWidth, intersectionHeight}}; + } + static Rect boundingRect( Point const &a, Point const &b, diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Transform.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/Transform.cpp index d9669ea349790e..56fb6431a0a836 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/Transform.cpp +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Transform.cpp @@ -366,22 +366,25 @@ Point operator*(Point const &point, Transform const &transform) { } Rect operator*(Rect const &rect, Transform const &transform) { - auto centre = rect.getCenter(); - - auto a = Point{rect.origin.x, rect.origin.y} - centre; - auto b = Point{rect.getMaxX(), rect.origin.y} - centre; - auto c = Point{rect.getMaxX(), rect.getMaxY()} - centre; - auto d = Point{rect.origin.x, rect.getMaxY()} - centre; - - auto vectorA = transform * Vector{a.x, a.y, 0, 1}; - auto vectorB = transform * Vector{b.x, b.y, 0, 1}; - auto vectorC = transform * Vector{c.x, c.y, 0, 1}; - auto vectorD = transform * Vector{d.x, d.y, 0, 1}; - - Point transformedA{vectorA.x + centre.x, vectorA.y + centre.y}; - Point transformedB{vectorB.x + centre.x, vectorB.y + centre.y}; - Point transformedC{vectorC.x + centre.x, vectorC.y + centre.y}; - Point transformedD{vectorD.x + centre.x, vectorD.y + centre.y}; + auto center = rect.getCenter(); + return transform.applyWithCenter(rect, center); +} + +Rect Transform::applyWithCenter(Rect const &rect, Point const ¢er) const { + auto a = Point{rect.origin.x, rect.origin.y} - center; + auto b = Point{rect.getMaxX(), rect.origin.y} - center; + auto c = Point{rect.getMaxX(), rect.getMaxY()} - center; + auto d = Point{rect.origin.x, rect.getMaxY()} - center; + + auto vectorA = *this * Vector{a.x, a.y, 0, 1}; + auto vectorB = *this * Vector{b.x, b.y, 0, 1}; + auto vectorC = *this * Vector{c.x, c.y, 0, 1}; + auto vectorD = *this * Vector{d.x, d.y, 0, 1}; + + Point transformedA{vectorA.x + center.x, vectorA.y + center.y}; + Point transformedB{vectorB.x + center.x, vectorB.y + center.y}; + Point transformedC{vectorC.x + center.x, vectorC.y + center.y}; + Point transformedD{vectorD.x + center.x, vectorD.y + center.y}; return Rect::boundingRect( transformedA, transformedB, transformedC, transformedD); diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/Transform.h b/packages/react-native/ReactCommon/react/renderer/graphics/Transform.h index c8c849c6677987..e0f29508f65c40 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/Transform.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/Transform.h @@ -155,6 +155,8 @@ struct Transform { */ Transform operator*(Transform const &rhs) const; + Rect applyWithCenter(Rect const &rect, Point const ¢er) const; + /** * Convert to folly::dynamic. */ diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/tests/TransformTest.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/tests/TransformTest.cpp index 70c232ecd019b8..dcbb3052e68b10 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/tests/TransformTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/graphics/tests/TransformTest.cpp @@ -41,6 +41,22 @@ TEST(TransformTest, scalingRect) { EXPECT_EQ(transformedRect.size.height, 200); } +TEST(TransformTest, scalingRectWithDifferentCenter) { + auto point = facebook::react::Point{100, 200}; + auto size = facebook::react::Size{300, 400}; + auto rect = facebook::react::Rect{point, size}; + + auto center = facebook::react::Point{0, 0}; + + auto transformedRect = + Transform::Scale(0.5, 0.5, 1).applyWithCenter(rect, center); + + EXPECT_EQ(transformedRect.origin.x, 50); + EXPECT_EQ(transformedRect.origin.y, 100); + EXPECT_EQ(transformedRect.size.width, 150); + EXPECT_EQ(transformedRect.size.height, 200); +} + TEST(TransformTest, invertingSize) { auto size = facebook::react::Size{300, 400}; auto transformedSize = size * Transform::VerticalInversion(); diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/MountingCoordinator.cpp b/packages/react-native/ReactCommon/react/renderer/mounting/MountingCoordinator.cpp index 88242a74cb5bbd..407535f412cac0 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/MountingCoordinator.cpp +++ b/packages/react-native/ReactCommon/react/renderer/mounting/MountingCoordinator.cpp @@ -179,6 +179,10 @@ TelemetryController const &MountingCoordinator::getTelemetryController() const { return telemetryController_; } +ShadowTreeRevision const &MountingCoordinator::getBaseRevision() const { + return baseRevision_; +} + void MountingCoordinator::setMountingOverrideDelegate( std::weak_ptr delegate) const { std::lock_guard lock(mutex_); diff --git a/packages/react-native/ReactCommon/react/renderer/mounting/MountingCoordinator.h b/packages/react-native/ReactCommon/react/renderer/mounting/MountingCoordinator.h index 83e432a66b46a6..7ca6aeb5a9b3c3 100644 --- a/packages/react-native/ReactCommon/react/renderer/mounting/MountingCoordinator.h +++ b/packages/react-native/ReactCommon/react/renderer/mounting/MountingCoordinator.h @@ -71,6 +71,8 @@ class MountingCoordinator final { TelemetryController const &getTelemetryController() const; + ShadowTreeRevision const &getBaseRevision() const; + /* * Methods from this section are meant to be used by * `MountingOverrideDelegate` only. diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 2d21cb6c98204e..1d812fcccf845a 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -114,7 +114,7 @@ Scheduler::Scheduler( commitHooks_ = schedulerToolbox.commitHooks; uiManager_ = uiManager; - for (auto const &commitHook : commitHooks_) { + for (auto &commitHook : commitHooks_) { uiManager->registerCommitHook(*commitHook); } @@ -147,7 +147,7 @@ Scheduler::~Scheduler() { LOG(WARNING) << "Scheduler::~Scheduler() was called (address: " << this << ")."; - for (auto const &commitHook : commitHooks_) { + for (auto &commitHook : commitHooks_) { uiManager_->unregisterCommitHook(*commitHook); } @@ -377,6 +377,10 @@ void Scheduler::uiManagerDidSetIsJSResponder( } } +void Scheduler::reportMount(SurfaceId surfaceId) const { + uiManager_->reportMount(surfaceId); +} + ContextContainer::Shared Scheduler::getContextContainer() const { return contextContainer_; } diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h index 6decb8e95df050..53a14e755138a2 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -108,6 +108,8 @@ class Scheduler final : public UIManagerDelegate { #pragma mark - UIManager std::shared_ptr getUIManager() const; + void reportMount(SurfaceId surfaceId) const; + #pragma mark - Event listeners void addEventListener(const std::shared_ptr &listener); void removeEventListener( @@ -122,7 +124,7 @@ class Scheduler final : public UIManagerDelegate { std::shared_ptr uiManager_; std::shared_ptr reactNativeConfig_; - std::vector> commitHooks_; + std::vector> commitHooks_; /* * At some point, we have to have an owning shared pointer to something that diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerToolbox.h b/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerToolbox.h index 844da3aff5baf7..2ada81212d1154 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerToolbox.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/SchedulerToolbox.h @@ -75,7 +75,7 @@ struct SchedulerToolbox final { /* * A list of `UIManagerCommitHook`s that should be registered in `UIManager`. */ - std::vector> commitHooks; + std::vector> commitHooks; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/timeline/TimelineController.cpp b/packages/react-native/ReactCommon/react/renderer/timeline/TimelineController.cpp index 68749cc6c2e485..dd57e7b9caacd1 100644 --- a/packages/react-native/ReactCommon/react/renderer/timeline/TimelineController.cpp +++ b/packages/react-native/ReactCommon/react/renderer/timeline/TimelineController.cpp @@ -42,19 +42,19 @@ void TimelineController::disable(TimelineHandler &&handler) const { } void TimelineController::commitHookWasRegistered( - UIManager const &uiManager) const noexcept { + UIManager const &uiManager) noexcept { uiManager_ = &uiManager; } void TimelineController::commitHookWasUnregistered( - UIManager const & /*uiManager*/) const noexcept { + UIManager const & /*uiManager*/) noexcept { uiManager_ = nullptr; } RootShadowNode::Unshared TimelineController::shadowTreeWillCommit( ShadowTree const &shadowTree, RootShadowNode::Shared const &oldRootShadowNode, - RootShadowNode::Unshared const &newRootShadowNode) const noexcept { + RootShadowNode::Unshared const &newRootShadowNode) noexcept { std::shared_lock lock(timelinesMutex_); assert(uiManager_ && "`uiManager_` must not be `nullptr`."); diff --git a/packages/react-native/ReactCommon/react/renderer/timeline/TimelineController.h b/packages/react-native/ReactCommon/react/renderer/timeline/TimelineController.h index fe09e203cc9f18..c69a247791fb87 100644 --- a/packages/react-native/ReactCommon/react/renderer/timeline/TimelineController.h +++ b/packages/react-native/ReactCommon/react/renderer/timeline/TimelineController.h @@ -52,14 +52,11 @@ class TimelineController final : public UIManagerCommitHook { RootShadowNode::Unshared shadowTreeWillCommit( ShadowTree const &shadowTree, RootShadowNode::Shared const &oldRootShadowNode, - RootShadowNode::Unshared const &newRootShadowNode) - const noexcept override; + RootShadowNode::Unshared const &newRootShadowNode) noexcept override; - void commitHookWasRegistered( - UIManager const &uiManager) const noexcept override; + void commitHookWasRegistered(UIManager const &uiManager) noexcept override; - void commitHookWasUnregistered( - UIManager const &uiManager) const noexcept override; + void commitHookWasUnregistered(UIManager const &uiManager) noexcept override; private: /* diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt b/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt index 5545fb0405e666..9370ff520b7b62 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/CMakeLists.txt @@ -33,6 +33,7 @@ target_link_libraries(react_render_uimanager react_render_runtimescheduler react_render_mounting react_config + reactnative rrc_root rrc_view runtimeexecutor diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 2063d246c2cebd..09c92088aff2e5 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -7,6 +7,7 @@ #include "UIManager.h" +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include @@ -592,8 +594,7 @@ ShadowTreeRegistry const &UIManager::getShadowTreeRegistry() const { return shadowTreeRegistry_; } -void UIManager::registerCommitHook( - UIManagerCommitHook const &commitHook) const { +void UIManager::registerCommitHook(UIManagerCommitHook &commitHook) { std::unique_lock lock(commitHookMutex_); react_native_assert( std::find(commitHooks_.begin(), commitHooks_.end(), &commitHook) == @@ -602,8 +603,7 @@ void UIManager::registerCommitHook( commitHooks_.push_back(&commitHook); } -void UIManager::unregisterCommitHook( - UIManagerCommitHook const &commitHook) const { +void UIManager::unregisterCommitHook(UIManagerCommitHook &commitHook) { std::unique_lock lock(commitHookMutex_); auto iterator = std::find(commitHooks_.begin(), commitHooks_.end(), &commitHook); @@ -612,6 +612,21 @@ void UIManager::unregisterCommitHook( commitHook.commitHookWasUnregistered(*this); } +void UIManager::registerMountHook(UIManagerMountHook &mountHook) { + std::unique_lock lock(mountHookMutex_); + react_native_assert( + std::find(mountHooks_.begin(), mountHooks_.end(), &mountHook) == + mountHooks_.end()); + mountHooks_.push_back(&mountHook); +} + +void UIManager::unregisterMountHook(UIManagerMountHook &mountHook) { + std::unique_lock lock(mountHookMutex_); + auto iterator = std::find(mountHooks_.begin(), mountHooks_.end(), &mountHook); + react_native_assert(iterator != mountHooks_.end()); + mountHooks_.erase(iterator); +} + #pragma mark - ShadowTreeDelegate RootShadowNode::Unshared UIManager::shadowTreeWillCommit( @@ -621,7 +636,7 @@ RootShadowNode::Unshared UIManager::shadowTreeWillCommit( std::shared_lock lock(commitHookMutex_); auto resultRootShadowNode = newRootShadowNode; - for (auto const *commitHook : commitHooks_) { + for (auto *commitHook : commitHooks_) { resultRootShadowNode = commitHook->shadowTreeWillCommit( shadowTree, oldRootShadowNode, resultRootShadowNode); } @@ -640,6 +655,28 @@ void UIManager::shadowTreeDidFinishTransaction( } } +void UIManager::reportMount(SurfaceId surfaceId) const { + auto time = JSExecutor::performanceNow(); + + auto rootShadowNode = RootShadowNode::Shared{}; + shadowTreeRegistry_.visit(surfaceId, [&](ShadowTree const &shadowTree) { + rootShadowNode = + shadowTree.getMountingCoordinator()->getBaseRevision().rootShadowNode; + }); + + if (!rootShadowNode) { + return; + } + + { + std::shared_lock lock(mountHookMutex_); + + for (auto *mountHook : mountHooks_) { + mountHook->shadowTreeDidMount(rootShadowNode, time); + } + } +} + #pragma mark - UIManagerAnimationDelegate void UIManager::setAnimationDelegate(UIManagerAnimationDelegate *delegate) { diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index e4f89e855c153f..97db13c7927742 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -30,6 +30,7 @@ namespace facebook::react { class UIManagerBinding; class UIManagerCommitHook; +class UIManagerMountHook; class UIManager final : public ShadowTreeDelegate { public: @@ -79,8 +80,14 @@ class UIManager final : public ShadowTreeDelegate { /* * Registers and unregisters a commit hook. */ - void registerCommitHook(UIManagerCommitHook const &commitHook) const; - void unregisterCommitHook(UIManagerCommitHook const &commitHook) const; + void registerCommitHook(UIManagerCommitHook &commitHook); + void unregisterCommitHook(UIManagerCommitHook &commitHook); + + /* + * Registers and unregisters a mount hook. + */ + void registerMountHook(UIManagerMountHook &mountHook); + void unregisterMountHook(UIManagerMountHook &mountHook); ShadowNode::Shared getNewestCloneOfShadowNode( ShadowNode const &shadowNode) const; @@ -191,6 +198,8 @@ class UIManager final : public ShadowTreeDelegate { ShadowTreeRegistry const &getShadowTreeRegistry() const; + void reportMount(SurfaceId surfaceId) const; + private: friend class UIManagerBinding; friend class Scheduler; @@ -215,7 +224,10 @@ class UIManager final : public ShadowTreeDelegate { ContextContainer::Shared contextContainer_; mutable std::shared_mutex commitHookMutex_; - mutable std::vector commitHooks_; + mutable std::vector commitHooks_; + + mutable std::shared_mutex mountHookMutex_; + mutable std::vector mountHooks_; std::unique_ptr leakChecker_; }; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerCommitHook.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerCommitHook.h index c6fdfaab4e0579..9424cb79123d68 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerCommitHook.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerCommitHook.h @@ -22,10 +22,9 @@ class UIManagerCommitHook { /* * Called right after the commit hook is registered or unregistered. */ - virtual void commitHookWasRegistered( - UIManager const &uiManager) const noexcept = 0; + virtual void commitHookWasRegistered(UIManager const &uiManager) noexcept = 0; virtual void commitHookWasUnregistered( - UIManager const &uiManager) const noexcept = 0; + UIManager const &uiManager) noexcept = 0; /* * Called right before a `ShadowTree` commits a new tree. @@ -35,7 +34,7 @@ class UIManagerCommitHook { virtual RootShadowNode::Unshared shadowTreeWillCommit( ShadowTree const &shadowTree, RootShadowNode::Shared const &oldRootShadowNode, - RootShadowNode::Unshared const &newRootShadowNode) const noexcept = 0; + RootShadowNode::Unshared const &newRootShadowNode) noexcept = 0; virtual ~UIManagerCommitHook() noexcept = default; }; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerMountHook.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerMountHook.h new file mode 100644 index 00000000000000..9f94f4618f503b --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerMountHook.h @@ -0,0 +1,34 @@ +/* + * 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 "UIManager.h" + +namespace facebook::react { + +class ShadowTree; +class UIManager; + +/* + * Implementing a mount hook allows to observe Shadow Trees being mounted in + * the host platform. + */ +class UIManagerMountHook { + public: + /* + * Called right after a `ShadowTree` is mounted in the host platform. + */ + virtual void shadowTreeDidMount( + RootShadowNode::Shared const &rootShadowNode, + double mountTime) noexcept = 0; + + virtual ~UIManagerMountHook() noexcept = default; +}; + +} // namespace facebook::react