diff --git a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js index 321bf45402789b..69cb482e9c300c 100644 --- a/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js +++ b/packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js @@ -170,6 +170,34 @@ export default class ReadOnlyElement extends ReadOnlyNode { getClientRects(): DOMRectList { throw new TypeError('Unimplemented'); } + + /** + * Pointer Capture APIs + */ + hasPointerCapture(pointerId: number): boolean { + const node = getShadowNode(this); + if (node != null) { + return nullthrows(getFabricUIManager()).hasPointerCapture( + node, + pointerId, + ); + } + return false; + } + + setPointerCapture(pointerId: number): void { + const node = getShadowNode(this); + if (node != null) { + nullthrows(getFabricUIManager()).setPointerCapture(node, pointerId); + } + } + + releasePointerCapture(pointerId: number): void { + const node = getShadowNode(this); + if (node != null) { + nullthrows(getFabricUIManager()).releasePointerCapture(node, pointerId); + } + } } function getChildElements(node: ReadOnlyNode): $ReadOnlyArray { diff --git a/packages/react-native/Libraries/ReactNative/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/FabricUIManager.js index ad6c089b78fe02..eab57429cb4acb 100644 --- a/packages/react-native/Libraries/ReactNative/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/FabricUIManager.js @@ -91,6 +91,13 @@ export interface Spec { +getScrollPosition: ( node: Node, ) => ?[/* scrollLeft: */ number, /* scrollTop: */ number]; + + /** + * Support methods for the Pointer Capture APIs. + */ + +hasPointerCapture: (node: Node, pointerId: number) => boolean; + +setPointerCapture: (node: Node, pointerId: number) => void; + +releasePointerCapture: (node: Node, pointerId: number) => void; } let nativeFabricUIManagerProxy: ?Spec; diff --git a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js index ddab78a2f14e97..5aebd90a205fde 100644 --- a/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js +++ b/packages/react-native/Libraries/ReactNative/__mocks__/FabricUIManager.js @@ -312,6 +312,9 @@ const FabricUIManagerMock: IFabricUIManagerMock = { return [x, y, width, height]; }, ), + hasPointerCapture: jest.fn((node: Node, pointerId: number): boolean => false), + setPointerCapture: jest.fn((node: Node, pointerId: number): void => {}), + releasePointerCapture: jest.fn((node: Node, pointerId: number): void => {}), setNativeProps: jest.fn((node: Node, newProps: NodeProps): void => {}), dispatchCommand: jest.fn( (node: Node, commandName: string, args: Array): void => {}, diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp index cf988ed287b96e..6d69cc9c83c1c5 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.cpp @@ -150,4 +150,8 @@ void EventEmitter::setEnabled(bool enabled) const { } } +const SharedEventTarget &EventEmitter::getEventTarget() const { + return eventTarget_; +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h index 77b859cf266ead..dfc2e5e08d785f 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventEmitter.h @@ -56,6 +56,8 @@ class EventEmitter { */ void setEnabled(bool enabled) const; + SharedEventTarget const &getEventTarget() const; + protected: #ifdef ANDROID // We need this temporarily due to lack of Java-counterparts for particular diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventTarget.cpp b/packages/react-native/ReactCommon/react/renderer/core/EventTarget.cpp index 82118ceee8b89e..e644287314baae 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventTarget.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/EventTarget.cpp @@ -26,7 +26,10 @@ void EventTarget::retain(jsi::Runtime &runtime) const { return; } - strongInstanceHandle_ = instanceHandle_->getInstanceHandle(runtime); + if (retainCount_ == 0) { + strongInstanceHandle_ = instanceHandle_->getInstanceHandle(runtime); + } + retainCount_ += 1; // Having a `null` or `undefined` object here indicates that // `weakInstanceHandle_` was already deallocated. This should *not* happen by @@ -44,7 +47,12 @@ void EventTarget::release(jsi::Runtime & /*runtime*/) const { // The method does not use `jsi::Runtime` reference. // It takes it only to ensure thread-safety (if the caller has the reference, // we are on a proper thread). - strongInstanceHandle_ = jsi::Value::null(); + if (retainCount_ > 0) { + retainCount_ -= 1; + if (retainCount_ == 0) { + strongInstanceHandle_ = jsi::Value::null(); + } + } } jsi::Value EventTarget::getInstanceHandle(jsi::Runtime &runtime) const { diff --git a/packages/react-native/ReactCommon/react/renderer/core/EventTarget.h b/packages/react-native/ReactCommon/react/renderer/core/EventTarget.h index 1b023dc65105e8..27cfb2e7c96053 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/EventTarget.h +++ b/packages/react-native/ReactCommon/react/renderer/core/EventTarget.h @@ -68,6 +68,7 @@ class EventTarget { const InstanceHandle::Shared instanceHandle_; mutable bool enabled_{false}; // Protected by `EventEmitter::DispatchMutex()`. mutable jsi::Value strongInstanceHandle_; // Protected by `jsi::Runtime &`. + mutable int retainCount_{0}; // Protected by `jsi::Runtime &`. }; using SharedEventTarget = std::shared_ptr; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp index 04b409cffe7de2..56f3a10c3d0d3f 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.cpp @@ -9,15 +9,182 @@ namespace facebook::react { +static PointerEventTarget retargetPointerEvent( + PointerEvent const &event, + ShadowNode const &nodeToTarget, + UIManager const &uiManager) { + PointerEvent retargetedEvent(event); + + // TODO: is dereferencing latestNodeToTarget without null checking safe? + auto latestNodeToTarget = uiManager.getNewestCloneOfShadowNode(nodeToTarget); + + // Adjust offsetX/Y to be relative to the retargeted node + // HACK: This is a basic/incomplete implementation which simply subtracts + // the retargeted node's origin from the original event's client coordinates. + // More work will be needed to properly take non-trival transforms into + // account. + auto layoutMetrics = uiManager.getRelativeLayoutMetrics( + *latestNodeToTarget, nullptr, {/* .includeTransform */ true}); + retargetedEvent.offsetPoint = { + event.clientPoint.x - layoutMetrics.frame.origin.x, + event.clientPoint.y - layoutMetrics.frame.origin.y, + }; + + // Retrieve the event target of the retargeted node + auto retargetedEventTarget = + latestNodeToTarget->getEventEmitter()->getEventTarget(); + + PointerEventTarget result = {}; + result.event = retargetedEvent; + result.target = retargetedEventTarget; + return result; +} + +static ShadowNode::Shared getCaptureTargetOverride( + PointerIdentifier pointerId, + CaptureTargetOverrideRegistry ®istry) { + auto pendingPointerItr = registry.find(pointerId); + if (pendingPointerItr == registry.end()) { + return nullptr; + } + + ShadowNode::Weak maybeTarget = pendingPointerItr->second; + if (maybeTarget.expired()) { + // target has expired so it should functionally behave the same as if it + // was removed from the override list. + registry.erase(pointerId); + return nullptr; + } + + return maybeTarget.lock(); +} + void PointerEventsProcessor::interceptPointerEvent( jsi::Runtime &runtime, - EventTarget const *eventTarget, + EventTarget const *target, std::string const &type, ReactEventPriority priority, PointerEvent const &event, - DispatchEvent const &eventDispatcher) { - // TODO: implement pointer capture redirection - eventDispatcher(runtime, eventTarget, type, priority, event); + DispatchEvent const &eventDispatcher, + UIManager const &uiManager) { + // Process all pending pointer capture assignments + processPendingPointerCapture(event, runtime, eventDispatcher, uiManager); + + PointerEvent pointerEvent(event); + EventTarget const *eventTarget = target; + + // Retarget the event if it has a pointer capture override target + auto overrideTarget = getCaptureTargetOverride( + pointerEvent.pointerId, pendingPointerCaptureTargetOverrides_); + if (overrideTarget != nullptr && + overrideTarget->getTag() != eventTarget->getTag()) { + auto retargeted = retargetPointerEvent(pointerEvent, *overrideTarget, uiManager); + + pointerEvent = retargeted.event; + eventTarget = retargeted.target.get(); + } + + eventTarget->retain(runtime); + eventDispatcher(runtime, eventTarget, type, priority, pointerEvent); + eventTarget->release(runtime); + + // Implicit pointer capture release + if (overrideTarget != nullptr && + (type == "topPointerUp" || type == "topPointerCancel")) { + releasePointerCapture(pointerEvent.pointerId, overrideTarget.get()); + processPendingPointerCapture( + pointerEvent, runtime, eventDispatcher, uiManager); + } +} + +void PointerEventsProcessor::setPointerCapture( + PointerIdentifier pointerId, + ShadowNode::Shared const &shadowNode) { + // TODO: Throw DOMException with name "NotFoundError" when pointerId does not + // match any of the active pointers + pendingPointerCaptureTargetOverrides_[pointerId] = shadowNode; +} + +void PointerEventsProcessor::releasePointerCapture( + PointerIdentifier pointerId, + ShadowNode const *shadowNode) { + // TODO: Throw DOMException with name "NotFoundError" when pointerId does not + // match any of the active pointers + + // We only clear the pointer's capture target override if release was called + // on the shadowNode which has the capture override, otherwise the result + // should no-op + auto pendingTarget = getCaptureTargetOverride( + pointerId, pendingPointerCaptureTargetOverrides_); + if (pendingTarget != nullptr && + pendingTarget->getTag() == shadowNode->getTag()) { + pendingPointerCaptureTargetOverrides_.erase(pointerId); + } +} + +bool PointerEventsProcessor::hasPointerCapture( + PointerIdentifier pointerId, + ShadowNode const *shadowNode) { + ShadowNode::Shared pendingTarget = getCaptureTargetOverride( + pointerId, pendingPointerCaptureTargetOverrides_); + if (pendingTarget != nullptr) { + return pendingTarget->getTag() == shadowNode->getTag(); + } + return false; +} + +void PointerEventsProcessor::processPendingPointerCapture( + PointerEvent const &event, + jsi::Runtime &runtime, + DispatchEvent const &eventDispatcher, + UIManager const &uiManager) { + auto pendingOverride = getCaptureTargetOverride( + event.pointerId, pendingPointerCaptureTargetOverrides_); + bool hasPendingOverride = pendingOverride != nullptr; + + auto activeOverride = getCaptureTargetOverride( + event.pointerId, activePointerCaptureTargetOverrides_); + bool hasActiveOverride = activeOverride != nullptr; + + if (!hasPendingOverride && !hasActiveOverride) { + return; + } + + auto pendingOverrideTag = + (hasPendingOverride) ? pendingOverride->getTag() : -1; + auto activeOverrideTag = (hasActiveOverride) ? activeOverride->getTag() : -1; + + if (hasActiveOverride && activeOverrideTag != pendingOverrideTag) { + auto retargeted = retargetPointerEvent(event, *activeOverride, uiManager); + + retargeted.target->retain(runtime); + eventDispatcher( + runtime, + retargeted.target.get(), + "topLostPointerCapture", + ReactEventPriority::Discrete, + retargeted.event); + retargeted.target->release(runtime); + } + + if (hasPendingOverride && activeOverrideTag != pendingOverrideTag) { + auto retargeted = retargetPointerEvent(event, *pendingOverride, uiManager); + + retargeted.target->retain(runtime); + eventDispatcher( + runtime, + retargeted.target.get(), + "topGotPointerCapture", + ReactEventPriority::Discrete, + retargeted.event); + retargeted.target->release(runtime); + } + + if (!hasPendingOverride) { + activePointerCaptureTargetOverrides_.erase(event.pointerId); + } else { + activePointerCaptureTargetOverrides_[event.pointerId] = pendingOverride; + } } } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.h b/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.h index 87953bd88bff40..8acdbe7654f686 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/PointerEventsProcessor.h @@ -22,6 +22,16 @@ using DispatchEvent = std::function; +using PointerIdentifier = int; +using CaptureTargetOverrideRegistry = + std::unordered_map; + +// Helper struct to package a PointerEvent and SharedEventTarget together +struct PointerEventTarget { + PointerEvent event; + SharedEventTarget target; +}; + class PointerEventsProcessor final { public: void interceptPointerEvent( @@ -30,7 +40,28 @@ class PointerEventsProcessor final { std::string const &type, ReactEventPriority priority, PointerEvent const &event, - DispatchEvent const &eventDispatcher); + DispatchEvent const &eventDispatcher, + UIManager const &uiManager); + + void setPointerCapture( + PointerIdentifier pointerId, + ShadowNode::Shared const &shadowNode); + void releasePointerCapture( + PointerIdentifier pointerId, + ShadowNode const *shadowNode); + bool hasPointerCapture( + PointerIdentifier pointerId, + ShadowNode const *shadowNode); + + private: + void processPendingPointerCapture( + PointerEvent const &event, + jsi::Runtime &runtime, + DispatchEvent const &eventDispatcher, + UIManager const &uiManager); + + CaptureTargetOverrideRegistry pendingPointerCaptureTargetOverrides_; + CaptureTargetOverrideRegistry activePointerCaptureTargetOverrides_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index f44aef8ecedfe2..063bd537617f5f 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -109,7 +109,13 @@ void UIManagerBinding::dispatchEvent( runtime, eventTarget, type, priority, eventPayload); }; pointerEventsProcessor_.interceptPointerEvent( - runtime, eventTarget, type, priority, pointerEvent, dispatchCallback); + runtime, + eventTarget, + type, + priority, + pointerEvent, + dispatchCallback, + *uiManager_); } else { dispatchEventToJS(runtime, eventTarget, type, priority, eventPayload); } @@ -1203,6 +1209,66 @@ jsi::Value UIManagerBinding::get( }); } + /** + * Pointer Capture APIs + */ + if (methodName == "hasPointerCapture") { + auto paramCount = 2; + return jsi::Function::createFromHostFunction( + runtime, + name, + paramCount, + [this, methodName, paramCount]( + jsi::Runtime &runtime, + jsi::Value const & /*thisValue*/, + jsi::Value const *arguments, + size_t count) -> jsi::Value { + validateArgumentCount(runtime, methodName, paramCount, count); + bool isCapturing = pointerEventsProcessor_.hasPointerCapture( + static_cast(arguments[1].asNumber()), + shadowNodeFromValue(runtime, arguments[0]).get()); + return jsi::Value(isCapturing); + }); + } + + if (methodName == "setPointerCapture") { + auto paramCount = 2; + return jsi::Function::createFromHostFunction( + runtime, + name, + paramCount, + [this, methodName, paramCount]( + jsi::Runtime &runtime, + jsi::Value const & /*thisValue*/, + jsi::Value const *arguments, + size_t count) -> jsi::Value { + validateArgumentCount(runtime, methodName, paramCount, count); + pointerEventsProcessor_.setPointerCapture( + static_cast(arguments[1].asNumber()), + shadowNodeFromValue(runtime, arguments[0])); + return jsi::Value::undefined(); + }); + } + + if (methodName == "releasePointerCapture") { + auto paramCount = 2; + return jsi::Function::createFromHostFunction( + runtime, + name, + paramCount, + [this, methodName, paramCount]( + jsi::Runtime &runtime, + jsi::Value const & /*thisValue*/, + jsi::Value const *arguments, + size_t count) -> jsi::Value { + validateArgumentCount(runtime, methodName, paramCount, count); + pointerEventsProcessor_.releasePointerCapture( + static_cast(arguments[1].asNumber()), + shadowNodeFromValue(runtime, arguments[0]).get()); + return jsi::Value::undefined(); + }); + } + return jsi::Value::undefined(); }