diff --git a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp index 6fb69012df42..8190c7c2f76b 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.cpp @@ -51,4 +51,20 @@ std::optional NativeViewTransition::getViewTransitionInstance( return result; } +jsi::Value NativeViewTransition::findPseudoElementShadowNodeByTag( + jsi::Runtime& rt, + double reactTag) { + auto& uiManager = UIManagerBinding::getBinding(rt)->getUIManager(); + auto* viewTransitionDelegate = uiManager.getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + auto shadowNode = viewTransitionDelegate->findPseudoElementShadowNodeByTag( + static_cast(reactTag)); + if (shadowNode) { + return Bridging>::toJs(rt, shadowNode); + } + } + + return jsi::Value::null(); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h index bd5cb3b424f2..f3311f40bef3 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h +++ b/packages/react-native/ReactCommon/react/nativemodule/viewtransition/NativeViewTransition.h @@ -26,6 +26,8 @@ class NativeViewTransition : public NativeViewTransitionCxxSpec getViewTransitionInstance(jsi::Runtime &rt, const std::string &name, const std::string &pseudo); + + jsi::Value findPseudoElementShadowNodeByTag(jsi::Runtime &rt, double reactTag); }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 3a1393ef40b0..477ecb8a8820 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -160,9 +160,8 @@ Scheduler::Scheduler( // Initialize ViewTransitionModule if (ReactNativeFeatureFlags::viewTransitionEnabled()) { - viewTransitionModule_ = std::make_unique(); - viewTransitionModule_->setUIManager(uiManager_.get()); - uiManager_->setViewTransitionDelegate(viewTransitionModule_.get()); + viewTransitionModule_ = std::make_shared(); + viewTransitionModule_->initialize(uiManager_.get(), viewTransitionModule_); } uiManager->registerMountHook(*eventPerformanceLogger_); diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h index ad16e3e40879..00ed0f43ed06 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.h @@ -147,7 +147,7 @@ class Scheduler final : public UIManagerDelegate { RuntimeScheduler *runtimeScheduler_{nullptr}; - std::unique_ptr viewTransitionModule_; + std::shared_ptr viewTransitionModule_; mutable std::shared_mutex onSurfaceStartCallbackMutex_; OnSurfaceStartCallback onSurfaceStartCallback_; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index 1aa9fdf2ab24..9b6b08f4642b 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -981,6 +981,37 @@ jsi::Value UIManagerBinding::get( }); } + if (methodName == "createViewTransitionInstance") { + auto paramCount = 2; + return jsi::Function::createFromHostFunction( + runtime, + name, + paramCount, + [uiManager, methodName, paramCount]( + jsi::Runtime& runtime, + const jsi::Value& /*thisValue*/, + const jsi::Value* arguments, + size_t count) -> jsi::Value { + validateArgumentCount(runtime, methodName, paramCount, count); + + auto transitionName = arguments[0].isString() + ? stringFromValue(runtime, arguments[0]) + : ""; + auto pseudoElementTag = tagFromValue(arguments[1]); + + if (!transitionName.empty()) { + auto* viewTransitionDelegate = + uiManager->getViewTransitionDelegate(); + if (viewTransitionDelegate != nullptr) { + viewTransitionDelegate->createViewTransitionInstance( + transitionName, pseudoElementTag); + } + } + + return jsi::Value::undefined(); + }); + } + if (methodName == "cancelViewTransitionName") { auto paramCount = 2; return jsi::Function::createFromHostFunction( diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h index 9d4d83637f4c..d3685775c353 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerViewTransitionDelegate.h @@ -23,6 +23,8 @@ class UIManagerViewTransitionDelegate { { } + virtual void createViewTransitionInstance(const std::string & /*name*/, Tag /*pseudoElementTag*/) {} + virtual void cancelViewTransitionName(const ShadowNode &shadowNode, const std::string &name) {} virtual void restoreViewTransitionName(const ShadowNode &shadowNode) {} @@ -55,6 +57,16 @@ class UIManagerViewTransitionDelegate { { return std::nullopt; } + + // Similar to UIManager::findShadowNodeByTag, but searches all direct children + // of the root node (where pseudo-element nodes live) rather than just the + // first child. Pseudo-element nodes are appended as additional children of the + // root node, rather than inserted into the main React tree, to avoid + // disrupting the user-created component tree. + virtual std::shared_ptr findPseudoElementShadowNodeByTag(Tag /*tag*/) const + { + return nullptr; + } }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp index b4d759bf4950..d67ad6e24a88 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.cpp @@ -7,15 +7,51 @@ #include "ViewTransitionModule.h" -#include - +#include #include +#include +#include +#include #include namespace facebook::react { -void ViewTransitionModule::setUIManager(UIManager* uiManager) { +ViewTransitionModule::~ViewTransitionModule() { + if (uiManager_ != nullptr) { + if (uiManager_->getViewTransitionDelegate() == this) { + uiManager_->setViewTransitionDelegate(nullptr); + } + uiManager_->unregisterCommitHook(*this); + uiManager_ = nullptr; + } +} + +void ViewTransitionModule::initialize( + UIManager* uiManager, + std::weak_ptr weakThis) { + if (uiManager_ != nullptr) { + uiManager_->unregisterCommitHook(*this); + } uiManager_ = uiManager; + if (uiManager_ != nullptr) { + uiManager_->registerCommitHook(*this); + + // Register as MountingOverrideDelegate on existing surfaces + uiManager_->getShadowTreeRegistry().enumerate( + [weakThis](const ShadowTree& shadowTree, bool& /*stop*/) { + shadowTree.getMountingCoordinator()->setMountingOverrideDelegate( + weakThis); + }); + + // Register on surfaces started in the future + uiManager_->setOnSurfaceStartCallback( + [weakThis](const ShadowTree& shadowTree) { + shadowTree.getMountingCoordinator()->setMountingOverrideDelegate( + weakThis); + }); + + uiManager_->setViewTransitionDelegate(this); + } } void ViewTransitionModule::applyViewTransitionName( @@ -45,6 +81,14 @@ void ViewTransitionModule::applyViewTransitionName( AnimationKeyFrameView oldView{ .layoutMetrics = keyframeMetrics, .tag = tag, .surfaceId = surfaceId}; oldLayout_[name] = oldView; + + // TODO: capture bitmap snapshot of old view via platform + + if (auto it = oldPseudoElementNodesRepository_.find(name); + it != oldPseudoElementNodesRepository_.end()) { + oldPseudoElementNodes_[name] = it->second.node; + } + } else { AnimationKeyFrameView newView{ .layoutMetrics = keyframeMetrics, .tag = tag, .surfaceId = surfaceId}; @@ -52,6 +96,167 @@ void ViewTransitionModule::applyViewTransitionName( } } +void ViewTransitionModule::createViewTransitionInstance( + const std::string& name, + Tag pseudoElementTag) { + if (uiManager_ == nullptr) { + return; + } + + // if createViewTransitionInstance is called before transition started, it + // creates the old pseudo elements for exiting nodes that potentially + // participate in current transition that's about to happen; if called after + // transition started, it creates old pseudo elements for entering nodes, and + // will be used in next transition when these node are exiting + bool forNextTransition = false; + AnimationKeyFrameView view = {}; + auto it = oldLayout_.find(name); + if (it == oldLayout_.end()) { + forNextTransition = true; + if (auto newIt = newLayout_.find(name); newIt != newLayout_.end()) { + view = newIt->second; + } + } else { + view = it->second; + } + + // Build props: absolute position matching old element, non-interactive + if (pseudoElementTag > 0 && view.tag > 0) { + // Create a base node with layout props via createNode + // TODO: T262559684 created dedicated shadow node type for old pseudo + // element + auto rawProps = RawProps( + folly::dynamic::object("position", "absolute")( + "left", view.layoutMetrics.originFromRoot.x)( + "top", view.layoutMetrics.originFromRoot.y)( + "width", view.layoutMetrics.size.width)( + "height", view.layoutMetrics.size.height)("pointerEvents", "none")( + "opacity", 0)("collapsable", false)); + + auto baseNode = uiManager_->createNode( + pseudoElementTag, + "View", + view.surfaceId, + std::move(rawProps), + nullptr /* instanceHandle */); + + if (baseNode == nullptr) { + return; + } + + // Clone the shadow node — bitmap will be set by platform + auto pseudoElementNode = baseNode->clone({}); + + if (pseudoElementNode != nullptr) { + if (!forNextTransition) { + oldPseudoElementNodes_[name] = pseudoElementNode; + } + oldPseudoElementNodesRepository_[name] = InactivePseudoElement{ + .node = pseudoElementNode, .sourceTag = view.tag}; + } + } +} + +RootShadowNode::Unshared ViewTransitionModule::shadowTreeWillCommit( + const ShadowTree& shadowTree, + const RootShadowNode::Shared& /*oldRootShadowNode*/, + const RootShadowNode::Unshared& newRootShadowNode, + const ShadowTreeCommitOptions& /*commitOptions*/) noexcept { + if (oldPseudoElementNodes_.empty()) { + return newRootShadowNode; + } + + auto surfaceId = shadowTree.getSurfaceId(); + + // Collect pseudo-element nodes for this surface, skipping any that are + // already present in the children list (from a previous commit hook run). + const auto& existingChildren = newRootShadowNode->getChildren(); + std::unordered_set existingTags; + existingTags.reserve(existingChildren.size()); + for (const auto& child : existingChildren) { + existingTags.insert(child->getTag()); + } + + auto newChildren = + std::make_shared>>( + existingChildren); + bool appended = false; + for (const auto& [name, node] : oldPseudoElementNodes_) { + if (node->getSurfaceId() == surfaceId && + existingTags.find(node->getTag()) == existingTags.end()) { + newChildren->push_back(node); + appended = true; + } + } + + if (!appended) { + return newRootShadowNode; + } + + return std::make_shared( + *newRootShadowNode, + ShadowNodeFragment{ + .props = ShadowNodeFragment::propsPlaceholder(), + .children = newChildren, + }); +} + +bool ViewTransitionModule::shouldOverridePullTransaction() const { + return !oldPseudoElementNodesRepository_.empty(); +} + +std::optional ViewTransitionModule::pullTransaction( + SurfaceId surfaceId, + MountingTransaction::Number number, + const TransactionTelemetry& telemetry, + ShadowViewMutationList mutations) const { + for (const auto& mutation : mutations) { + if (mutation.type == ShadowViewMutation::Delete) { + auto tag = mutation.oldChildShadowView.tag; + for (auto it = oldPseudoElementNodesRepository_.begin(); + it != oldPseudoElementNodesRepository_.end();) { + if (it->second.sourceTag == tag) { + it = oldPseudoElementNodesRepository_.erase(it); + } else { + ++it; + } + } + } + } + return MountingTransaction{ + surfaceId, number, std::move(mutations), telemetry}; +} + +std::shared_ptr +ViewTransitionModule::findPseudoElementShadowNodeByTag(Tag tag) const { + if (uiManager_ == nullptr) { + return nullptr; + } + + auto shadowNode = std::shared_ptr{}; + + uiManager_->getShadowTreeRegistry().enumerate( + [&](const ShadowTree& shadowTree, bool& stop) { + const auto rootShadowNode = + shadowTree.getCurrentRevision().rootShadowNode; + + if (rootShadowNode != nullptr) { + const auto& children = rootShadowNode->getChildren(); + // Pseudo element nodes are appended after the first child (the main + // React tree), so iterate from index 1 onwards. + for (size_t i = 1; i < children.size(); ++i) { + if (children[i]->getTag() == tag) { + shadowNode = children[i]; + stop = true; + return; + } + } + } + }); + + return shadowNode; +} + void ViewTransitionModule::cancelViewTransitionName( const ShadowNode& shadowNode, const std::string& name) { @@ -67,6 +272,14 @@ void ViewTransitionModule::restoreViewTransitionName( cancelledNameRegistry_.erase(shadowNode.getTag()); } +void ViewTransitionModule::applySnapshotsOnPseudoElementShadowNodes() { + if (oldPseudoElementNodes_.empty() || uiManager_ == nullptr) { + return; + } + + // TODO: set bitmap snapshots on pseudo-element views via platform +} + LayoutMetrics ViewTransitionModule::captureLayoutMetricsFromRoot( const ShadowNode& shadowNode) { if (uiManager_ == nullptr) { @@ -100,13 +313,13 @@ void ViewTransitionModule::startViewTransition( // Mark transition as started transitionStarted_ = true; - // Call mutation callback (including commitRoot, measureInstance - // applyViewTransitionName for old & new) + // Call mutation callback (including commitRoot, measureInstance, + // applyViewTransitionName, createViewTransitionInstance for old & new) if (mutationCallback) { mutationCallback(); } - // TODO: capture pseudo elements + applySnapshotsOnPseudoElementShadowNodes(); if (onReadyCallback) { onReadyCallback(); @@ -128,6 +341,7 @@ void ViewTransitionModule::startViewTransitionEnd() { } } nameRegistry_.clear(); + oldPseudoElementNodes_.clear(); transitionStarted_ = false; } @@ -152,12 +366,16 @@ ViewTransitionModule::getViewTransitionInstance( auto it = oldLayout_.find(name); if (it != oldLayout_.end()) { const auto& view = it->second; + auto pseudoElementIt = oldPseudoElementNodes_.find(name); + auto nativeTag = pseudoElementIt != oldPseudoElementNodes_.end() + ? pseudoElementIt->second->getTag() + : view.tag; return ViewTransitionInstance{ .x = view.layoutMetrics.originFromRoot.x, .y = view.layoutMetrics.originFromRoot.y, .width = view.layoutMetrics.size.width, .height = view.layoutMetrics.size.height, - .nativeTag = view.tag}; + .nativeTag = nativeTag}; } } diff --git a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h index f5d1f59fdc50..ae585978b409 100644 --- a/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h +++ b/packages/react-native/ReactCommon/react/renderer/viewtransition/ViewTransitionModule.h @@ -11,24 +11,35 @@ #include #include +#include #include +#include #include namespace facebook::react { +class ShadowTree; class UIManager; -class ViewTransitionModule : public UIManagerViewTransitionDelegate { +class ViewTransitionModule : public UIManagerViewTransitionDelegate, + public UIManagerCommitHook, + public MountingOverrideDelegate { public: - ~ViewTransitionModule() override = default; + ~ViewTransitionModule() override; - void setUIManager(UIManager *uiManager); + void initialize(UIManager *uiManager, std::weak_ptr weakThis); + +#pragma mark - UIManagerViewTransitionDelegate // will be called when a view will transition. if a view already has a view-transition-name, it may not be called // again until it's removed void applyViewTransitionName(const ShadowNode &shadowNode, const std::string &name, const std::string &className) override; + // creates a pseudo-element shadow node for a given transition name using the + // captured old layout metrics + void createViewTransitionInstance(const std::string &name, Tag pseudoElementTag) override; + // if a viewTransitionName is cancelled, the element doesn't have view-transition-name and browser won't be taking // snapshot void cancelViewTransitionName(const ShadowNode &shadowNode, const std::string &name) override; @@ -46,6 +57,27 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate { std::optional getViewTransitionInstance(const std::string &name, const std::string &pseudo) override; +#pragma mark - UIManagerCommitHook + + void commitHookWasRegistered(const UIManager & /*uiManager*/) noexcept override {} + void commitHookWasUnregistered(const UIManager & /*uiManager*/) noexcept override {} + RootShadowNode::Unshared shadowTreeWillCommit( + const ShadowTree &shadowTree, + const RootShadowNode::Shared &oldRootShadowNode, + const RootShadowNode::Unshared &newRootShadowNode, + const ShadowTreeCommitOptions &commitOptions) noexcept override; + +#pragma mark - MountingOverrideDelegate + + bool shouldOverridePullTransaction() const override; + std::optional pullTransaction( + SurfaceId surfaceId, + MountingTransaction::Number number, + const TransactionTelemetry &telemetry, + ShadowViewMutationList mutations) const override; + + std::shared_ptr findPseudoElementShadowNodeByTag(Tag tag) const override; + // Animation state structure for storing minimal view data struct AnimationKeyFrameViewLayoutMetrics { Point originFromRoot; @@ -72,8 +104,23 @@ class ViewTransitionModule : public UIManagerViewTransitionDelegate { // used for cancel/restore viewTransitionName std::unordered_map> cancelledNameRegistry_{}; + // pseudo-element nodes keyed by transition name, appended to root children via UIManagerCommitHook + // TODO: T262559264 pseudo elements should be cleaned up as soon as transition animation ends + std::unordered_map> oldPseudoElementNodes_{}; + + struct InactivePseudoElement { + std::shared_ptr node; + Tag sourceTag{0}; // tag of the original view this was created from + }; + // pseudo-element nodes created for entering nodes, to be copied into + // oldPseudoElementNodes_ during the next applyViewTransitionName call. + // Mutable because pullTransaction (const) needs to erase unmounted entries. + mutable std::unordered_map oldPseudoElementNodesRepository_{}; + LayoutMetrics captureLayoutMetricsFromRoot(const ShadowNode &shadowNode); + void applySnapshotsOnPseudoElementShadowNodes(); + UIManager *uiManager_{nullptr}; bool transitionStarted_{false}; diff --git a/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js b/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js index 1c56f51e1f3e..c9fa79cd3639 100644 --- a/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js +++ b/packages/react-native/src/private/viewtransition/specs/NativeViewTransition.js @@ -23,6 +23,7 @@ export interface Spec extends TurboModule { height: number, nativeTag: number, }; + +findPseudoElementShadowNodeByTag: (reactTag: number) => ?unknown /* Node */; } export default TurboModuleRegistry.get(