Skip to content

Commit

Permalink
Add initial Pointer Capture API implementation (facebook#38505)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: facebook#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).

Differential Revision: D47533366

fbshipit-source-id: f30f4ec292897ad7aa7ab13a19b40ee81749f517
  • Loading branch information
vincentriemer committed Aug 3, 2023
1 parent 08688d3 commit 0fedc96
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 8 deletions.
28 changes: 28 additions & 0 deletions packages/react-native/Libraries/DOM/Nodes/ReadOnlyElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,8 @@ void EventEmitter::setEnabled(bool enabled) const {
}
}

const SharedEventTarget &EventEmitter::getEventTarget() const {
return eventTarget_;
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<const EventTarget>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 &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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ using DispatchEvent = std::function<void(
ReactEventPriority priority,
const EventPayload &payload)>;

using PointerIdentifier = int;
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(
Expand All @@ -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
Loading

0 comments on commit 0fedc96

Please sign in to comment.