Add initial Pointer Capture API implementation (#38505)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/38505

Changelog: [Internal] - Add initial Pointer Capture API implementation

This diff introduces the first baseline implementation of pointer capturing through intercepting/modifying Pointer Events in flight through the `PointerEventsProcessor` class.

Firstly: This adds and exposes the imperative methods `setPointerCapture`, `releasePointerCapture`, and `hasPointerCapture` which is added to the host `ReadOnlyElement` ref API. These methods are used to manage/query the `pendingPointerCaptureTargetOverrides_` map as [defined in the spec](https://www.w3.org/TR/pointerevents/#setting-pointer-capture).

The code is fairly self-explainatory when it comes to retargeting the events in `PointerEventsProcessor::interceptPointerEvent` but when it comes to firing the `gotPointerCapture` and `lostPointerCapture` events those are handled in `PointerEventsProcessor::processPendingPointerCapture` and is a fairly direct implementation of [the spec's pseudocode](https://www.w3.org/TR/pointerevents/#process-pending-pointer-capture).

Finally at the end of `interceptPointerEvent` I've included the basics of implicit pointer capture *release* as per [the spec](https://www.w3.org/TR/pointerevents/#implicit-release-of-pointer-capture) (note that implicit pointer capture is not yet implemented).

Reviewed By: rozele

Differential Revision: D47533366

fbshipit-source-id: 1786f9703a88201bc9c7bde61af76eb4b07a20ee
This commit is contained in:
Vincent Riemer
2023-08-10 14:53:31 -07:00
committed by Facebook GitHub Bot
parent 6860ffa350
commit 37fee66680
10 changed files with 326 additions and 8 deletions
@@ -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<ReadOnlyElement> {
@@ -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;
@@ -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<mixed>): void => {},
@@ -150,4 +150,8 @@ void EventEmitter::setEnabled(bool enabled) const {
}
}
const SharedEventTarget &EventEmitter::getEventTarget() const {
return eventTarget_;
}
} // namespace facebook::react
@@ -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
@@ -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) {
strongInstanceHandle_ = jsi::Value::null();
}
react_native_assert(retainCount_ >= 0);
}
jsi::Value EventTarget::getInstanceHandle(jsi::Runtime &runtime) const {
@@ -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 size_t retainCount_{0}; // Protected by `jsi::Runtime &`.
};
using SharedEventTarget = std::shared_ptr<const EventTarget>;
@@ -9,15 +9,183 @@
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 &registry) {
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
@@ -22,6 +22,16 @@ using DispatchEvent = std::function<void(
ReactEventPriority priority,
const EventPayload &payload)>;
using PointerIdentifier = int32_t;
using CaptureTargetOverrideRegistry =
std::unordered_map<PointerIdentifier, ShadowNode::Weak>;
// 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
@@ -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);
}
@@ -1201,6 +1207,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<int>(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<int>(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<int>(arguments[1].asNumber()),
shadowNodeFromValue(runtime, arguments[0]).get());
return jsi::Value::undefined();
});
}
return jsi::Value::undefined();
}