From 179c24e25518b4fc32b613fa9ade9c17d2ab299a Mon Sep 17 00:00:00 2001 From: Vincent Riemer Date: Thu, 7 Apr 2022 14:07:58 -0700 Subject: [PATCH] Emit touch-equivalent W3C pointer events on iOS Summary: Changelog: [Internal] Emit touch-equivalent W3C pointer events on iOS Reviewed By: lunaleaps Differential Revision: D35295104 fbshipit-source-id: 1c1d5a4159bbfed92df151f7e12a4973ec44e970 --- .../NativeComponent/PlatformBaseViewConfig.js | 26 ++++++++ React/Fabric/RCTSurfaceTouchHandler.mm | 55 +++++++++++++++++ React/Views/RCTViewManager.m | 6 ++ .../renderer/components/view/PointerEvent.cpp | 35 +++++++++++ .../renderer/components/view/PointerEvent.h | 59 +++++++++++++++++++ .../components/view/TouchEventEmitter.cpp | 58 ++++++++++++++++++ .../components/view/TouchEventEmitter.h | 11 ++++ packages/rn-tester/Podfile.lock | 2 +- 8 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 ReactCommon/react/renderer/components/view/PointerEvent.cpp create mode 100644 ReactCommon/react/renderer/components/view/PointerEvent.h diff --git a/Libraries/NativeComponent/PlatformBaseViewConfig.js b/Libraries/NativeComponent/PlatformBaseViewConfig.js index a0b501d2540..af14525f07d 100644 --- a/Libraries/NativeComponent/PlatformBaseViewConfig.js +++ b/Libraries/NativeComponent/PlatformBaseViewConfig.js @@ -363,6 +363,32 @@ const PlatformBaseViewConfig: PartialViewConfigWithoutName = captured: 'onTouchEndCapture', }, }, + + // Pointer Events + topPointerCancel: { + phasedRegistrationNames: { + captured: 'onPointerCancelCapture', + bubbled: 'onPointerCancel', + }, + }, + topPointerDown: { + phasedRegistrationNames: { + captured: 'onPointerDownCapture', + bubbled: 'onPointerDown', + }, + }, + topPointerMove2: { + phasedRegistrationNames: { + captured: 'onPointerMove2Capture', + bubbled: 'onPointerMove2', + }, + }, + topPointerUp: { + phasedRegistrationNames: { + captured: 'onPointerUpCapture', + bubbled: 'onPointerUp', + }, + }, }, directEventTypes: { topAccessibilityAction: { diff --git a/React/Fabric/RCTSurfaceTouchHandler.mm b/React/Fabric/RCTSurfaceTouchHandler.mm index b19d046e425..95e2d5e5de5 100644 --- a/React/Fabric/RCTSurfaceTouchHandler.mm +++ b/React/Fabric/RCTSurfaceTouchHandler.mm @@ -58,6 +58,11 @@ struct ActiveTouch { Touch touch; SharedTouchEventEmitter eventEmitter; + /* + * The type of touch received. + */ + UITouchType touchType; + /* * A component view on which the touch was begun. */ @@ -98,6 +103,8 @@ static void UpdateActiveTouchWithUITouch( if (RCTForceTouchAvailable()) { activeTouch.touch.force = RCTZeroIfNaN(uiTouch.force / uiTouch.maximumPossibleForce); } + + activeTouch.touchType = uiTouch.type; } static ActiveTouch CreateTouchWithUITouch(UITouch *uiTouch, UIView *rootComponentView, CGPoint rootViewOriginOffset) @@ -121,6 +128,35 @@ static ActiveTouch CreateTouchWithUITouch(UITouch *uiTouch, UIView *rootComponen return activeTouch; } +static const char *PointerTypeCStringFromUITouchType(UITouchType type) +{ + switch (type) { + case UITouchTypeDirect: + return "touch"; + case UITouchTypePencil: + return "pen"; + case UITouchTypeIndirectPointer: + return "mouse"; + case UITouchTypeIndirect: + default: + return ""; + } +} + +static PointerEvent CreatePointerEventFromActiveTouch(ActiveTouch activeTouch) +{ + Touch touch = activeTouch.touch; + + PointerEvent event = {}; + event.pointerId = touch.identifier; + event.pressure = touch.force; + event.pointerType = PointerTypeCStringFromUITouchType(activeTouch.touchType); + event.clientPoint = touch.pagePoint; + event.target = touch.target; + event.timestamp = touch.timestamp; + return event; +} + static BOOL AllTouchesAreCancelledOrEnded(NSSet *touches) { for (UITouch *touch in touches) { @@ -273,6 +309,25 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithTarget : (id)target action : (SEL)act changedActiveTouches.insert(activeTouch); event.changedTouches.insert(activeTouch.touch); uniqueEventEmitters.insert(activeTouch.eventEmitter); + + // emit w3c pointer events + if (RCTGetDispatchW3CPointerEvents()) { + PointerEvent pointerEvent = CreatePointerEventFromActiveTouch(activeTouch); + switch (eventType) { + case RCTTouchEventTypeTouchStart: + activeTouch.eventEmitter->onPointerDown(pointerEvent); + break; + case RCTTouchEventTypeTouchMove: + activeTouch.eventEmitter->onPointerMove2(pointerEvent); + break; + case RCTTouchEventTypeTouchEnd: + activeTouch.eventEmitter->onPointerUp(pointerEvent); + break; + case RCTTouchEventTypeTouchCancel: + activeTouch.eventEmitter->onPointerCancel(pointerEvent); + break; + } + } } for (const auto &pair : _activeTouches) { diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 7b1aa215af5..f41027c24ac 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -445,4 +445,10 @@ RCT_CUSTOM_VIEW_PROPERTY(onTouchMove, BOOL, RCTView) {} RCT_CUSTOM_VIEW_PROPERTY(onTouchEnd, BOOL, RCTView) {} RCT_CUSTOM_VIEW_PROPERTY(onTouchCancel, BOOL, RCTView) {} +// Pointer Events +RCT_EXPORT_VIEW_PROPERTY(onPointerCancel, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onPointerDown, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onPointerMove2, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onPointerUp, RCTBubblingEventBlock) + @end diff --git a/ReactCommon/react/renderer/components/view/PointerEvent.cpp b/ReactCommon/react/renderer/components/view/PointerEvent.cpp new file mode 100644 index 00000000000..5b5e1128390 --- /dev/null +++ b/ReactCommon/react/renderer/components/view/PointerEvent.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "PointerEvent.h" + +namespace facebook { +namespace react { + +#if RN_DEBUG_STRING_CONVERTIBLE + +std::string getDebugName(PointerEvent const &pointerEvent) { + return "PointerEvent"; +} + +std::vector getDebugProps( + PointerEvent const &pointerEvent, + DebugStringConvertibleOptions options) { + return { + {"pointerId", getDebugDescription(pointerEvent.pointerId, options)}, + {"pressure", getDebugDescription(pointerEvent.pressure, options)}, + {"pointerType", getDebugDescription(pointerEvent.pointerType, options)}, + {"clientPoint", getDebugDescription(pointerEvent.clientPoint, options)}, + {"target", getDebugDescription(pointerEvent.target, options)}, + {"timestamp", getDebugDescription(pointerEvent.timestamp, options)}, + }; +} + +#endif + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/react/renderer/components/view/PointerEvent.h b/ReactCommon/react/renderer/components/view/PointerEvent.h new file mode 100644 index 00000000000..f9094b86696 --- /dev/null +++ b/ReactCommon/react/renderer/components/view/PointerEvent.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include + +namespace facebook { +namespace react { + +struct PointerEvent { + /* + * A unique identifier for the pointer causing the event. + */ + int pointerId; + /* + * The normalized pressure of the pointer input in the range 0 to 1, where 0 + * and 1 represent the minimum and maximum pressure the hardware is capable of + * detecting, respectively. + */ + Float pressure; + /* + * Indicates the device type that caused the event (mouse, pen, touch, etc.) + */ + std::string pointerType; + /* + * Point within the application's viewport at which the event occurred (as + * opposed to the coordinate within the page). + */ + Point clientPoint; + /* + * A reference to the view to which the event was originally dispatched. + */ + Tag target; + /* + * The time at which the event was created (in milliseconds). By + * specification, this value is time since epoch—but in reality, browsers' + * definitions vary. + */ + Float timestamp; +}; + +#if RN_DEBUG_STRING_CONVERTIBLE + +std::string getDebugName(PointerEvent const &pointerEvent); +std::vector getDebugProps( + PointerEvent const &pointerEvent, + DebugStringConvertibleOptions options); + +#endif + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp b/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp index 31184c167ed..d2436fd0969 100644 --- a/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp +++ b/ReactCommon/react/renderer/components/view/TouchEventEmitter.cpp @@ -59,6 +59,20 @@ static jsi::Value touchEventPayload( return object; } +static jsi::Value pointerEventPayload( + jsi::Runtime &runtime, + PointerEvent const &event) { + auto object = jsi::Object(runtime); + object.setProperty(runtime, "pointerId", event.pointerId); + object.setProperty(runtime, "pressure", event.pressure); + object.setProperty(runtime, "pointerType", event.pointerType); + object.setProperty(runtime, "clientX", event.clientPoint.x); + object.setProperty(runtime, "clientY", event.clientPoint.y); + object.setProperty(runtime, "target", event.target); + object.setProperty(runtime, "timestamp", event.timestamp * 1000); + return object; +} + void TouchEventEmitter::dispatchTouchEvent( std::string type, TouchEvent const &event, @@ -73,6 +87,20 @@ void TouchEventEmitter::dispatchTouchEvent( category); } +void TouchEventEmitter::dispatchPointerEvent( + std::string type, + PointerEvent const &event, + EventPriority priority, + RawEvent::Category category) const { + dispatchEvent( + std::move(type), + [event](jsi::Runtime &runtime) { + return pointerEventPayload(runtime, event); + }, + priority, + category); +} + void TouchEventEmitter::onTouchStart(TouchEvent const &event) const { dispatchTouchEvent( "touchStart", @@ -103,5 +131,35 @@ void TouchEventEmitter::onTouchCancel(TouchEvent const &event) const { RawEvent::Category::ContinuousEnd); } +void TouchEventEmitter::onPointerCancel(const PointerEvent &event) const { + dispatchPointerEvent( + "pointerCancel", + event, + EventPriority::AsynchronousBatched, + RawEvent::Category::ContinuousEnd); +} + +void TouchEventEmitter::onPointerDown(const PointerEvent &event) const { + dispatchPointerEvent( + "pointerDown", + event, + EventPriority::AsynchronousBatched, + RawEvent::Category::ContinuousStart); +} + +void TouchEventEmitter::onPointerMove2(const PointerEvent &event) const { + dispatchUniqueEvent("pointerMove2", [event](jsi::Runtime &runtime) { + return pointerEventPayload(runtime, event); + }); +} + +void TouchEventEmitter::onPointerUp(const PointerEvent &event) const { + dispatchPointerEvent( + "pointerUp", + event, + EventPriority::AsynchronousBatched, + RawEvent::Category::ContinuousEnd); +} + } // namespace react } // namespace facebook diff --git a/ReactCommon/react/renderer/components/view/TouchEventEmitter.h b/ReactCommon/react/renderer/components/view/TouchEventEmitter.h index 3978c1e979b..25e884fbeeb 100644 --- a/ReactCommon/react/renderer/components/view/TouchEventEmitter.h +++ b/ReactCommon/react/renderer/components/view/TouchEventEmitter.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -29,12 +30,22 @@ class TouchEventEmitter : public EventEmitter { void onTouchEnd(TouchEvent const &event) const; void onTouchCancel(TouchEvent const &event) const; + void onPointerCancel(PointerEvent const &event) const; + void onPointerDown(PointerEvent const &event) const; + void onPointerMove2(PointerEvent const &event) const; + void onPointerUp(PointerEvent const &event) const; + private: void dispatchTouchEvent( std::string type, TouchEvent const &event, EventPriority priority, RawEvent::Category category) const; + void dispatchPointerEvent( + std::string type, + PointerEvent const &event, + EventPriority priority, + RawEvent::Category category) const; }; } // namespace react diff --git a/packages/rn-tester/Podfile.lock b/packages/rn-tester/Podfile.lock index a3a62879859..935d93bfc36 100644 --- a/packages/rn-tester/Podfile.lock +++ b/packages/rn-tester/Podfile.lock @@ -942,6 +942,6 @@ SPEC CHECKSUMS: Yoga: cdc7a2377fc03a2d44322b7784a2461b4c17da09 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 0655d9d79b9837da383b58df5ece7f5c0c4f45b0 +PODFILE CHECKSUM: 22e00978a86d653e611654e52ad29190769af232 COCOAPODS: 1.11.2