mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
6860ffa350
commit
37fee66680
@@ -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>;
|
||||
|
||||
+172
-4
@@ -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 ®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
|
||||
|
||||
+32
-1
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user