Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions packages/pointer-native-drawing/ios/PencilGestureRecognizer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@class PencilGestureRecognizer;

/// Data collected from a single gesture recognizer callback,
/// including all coalesced touch samples.
@interface PencilTouchData : NSObject

@property (nonatomic, readonly) NSArray<UITouch *> *coalescedTouches;
@property (nonatomic, readonly) NSArray<UITouch *> *predictedTouches;
@property (nonatomic, readonly) UIView *referenceView;
@property (nonatomic, readonly) UITouchType touchType;

- (instancetype)initWithCoalescedTouches:(NSArray<UITouch *> *)coalesced
predictedTouches:(NSArray<UITouch *> *)predicted
referenceView:(UIView *)view
touchType:(UITouchType)touchType;

@end

/// Protocol for receiving structured pencil touch data with coalesced samples.
@protocol PencilGestureRecognizerDelegate <NSObject>

- (void)pencilRecognizer:(PencilGestureRecognizer *)recognizer
touchBeganWith:(PencilTouchData *)data;
- (void)pencilRecognizer:(PencilGestureRecognizer *)recognizer
touchMovedWith:(PencilTouchData *)data;
- (void)pencilRecognizer:(PencilGestureRecognizer *)recognizer
touchEndedWith:(PencilTouchData *)data;
- (void)pencilRecognizer:(PencilGestureRecognizer *)recognizer
touchCancelledWith:(PencilTouchData *)data;

@end

/// A gesture recognizer that recognizes Apple Pencil touches and,
/// when acceptsFingerInput is YES, also direct (finger) touches.
/// When finger input is disabled, finger touches are immediately
/// failed so they fall through to underlying gesture recognizers
/// (e.g. RNGH PanGestureHandler).
///
/// Coalesced touch data is forwarded via the pencilDelegate because
/// UIGestureRecognizer action selectors do not receive the UIEvent.
@interface PencilGestureRecognizer : UIGestureRecognizer

@property (nonatomic, weak, nullable) id<PencilGestureRecognizerDelegate> pencilDelegate;

/// When YES, finger (UITouchTypeDirect) touches are accepted in
/// addition to pencil touches. Default is NO (pencil only).
@property (nonatomic) BOOL acceptsFingerInput;

@end

NS_ASSUME_NONNULL_END
191 changes: 191 additions & 0 deletions packages/pointer-native-drawing/ios/PencilGestureRecognizer.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#import "PencilGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>

@implementation PencilTouchData

- (instancetype)initWithCoalescedTouches:(NSArray<UITouch *> *)coalesced
predictedTouches:(NSArray<UITouch *> *)predicted
referenceView:(UIView *)view
touchType:(UITouchType)touchType
{
if (self = [super init]) {
_coalescedTouches = [coalesced copy];
_predictedTouches = [predicted copy];
_referenceView = view;
_touchType = touchType;
}
return self;
}

@end

@implementation PencilGestureRecognizer {
UITouch *_trackedTouch;
UITouchType _trackedTouchType;
PencilTouchData *_pendingBeganData; // deferred began for finger input
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (_trackedTouch != nil) {
// Already tracking a touch — handle additional fingers
if (self.acceptsFingerInput) {
if (_pendingBeganData) {
// Drawing never actually started — silently fail so RNGH zoom/pan takes over
_pendingBeganData = nil;
_trackedTouch = nil;
self.state = UIGestureRecognizerStateFailed;
return;
}
// Drawing already started — cancel it
PencilTouchData *data = [[PencilTouchData alloc]
initWithCoalescedTouches:@[_trackedTouch]
predictedTouches:@[]
referenceView:self.view
touchType:_trackedTouchType];
[self.pencilDelegate pencilRecognizer:self touchCancelledWith:data];
self.state = UIGestureRecognizerStateCancelled;
_trackedTouch = nil;
return;
}
// Pencil-only mode: ignore extra touches (existing behavior)
for (UITouch *touch in touches) {
[self ignoreTouch:touch forEvent:event];
}
return;
}

for (UITouch *touch in touches) {
BOOL isAccepted = (touch.type == UITouchTypePencil) ||
(self.acceptsFingerInput && touch.type == UITouchTypeDirect);
if (isAccepted) {
_trackedTouch = touch;
_trackedTouchType = touch.type;

NSArray<UITouch *> *coalesced = [event coalescedTouchesForTouch:touch] ?: @[touch];
NSArray<UITouch *> *predicted = [event predictedTouchesForTouch:touch] ?: @[];
PencilTouchData *data = [[PencilTouchData alloc] initWithCoalescedTouches:coalesced
predictedTouches:predicted
referenceView:self.view
touchType:_trackedTouchType];

if (touch.type == UITouchTypeDirect) {
// Finger: defer Began until first touchesMoved — gives time for
// a 2nd finger to arrive (zoom/pan intent) before committing to draw.
// Stay in Possible so we can transition to Failed if 2nd finger arrives.
_pendingBeganData = data;
return;
}

// Pencil: immediate began (always drawing intent)
self.state = UIGestureRecognizerStateBegan;
[self.pencilDelegate pencilRecognizer:self touchBeganWith:data];
return;
}
}

self.state = UIGestureRecognizerStateFailed;
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (_trackedTouch == nil) {
return;
}

for (UITouch *touch in touches) {
if (touch == _trackedTouch) {
// Flush deferred began before emitting moved
if (_pendingBeganData) {
self.state = UIGestureRecognizerStateBegan;
[self.pencilDelegate pencilRecognizer:self touchBeganWith:_pendingBeganData];
_pendingBeganData = nil;
} else {
self.state = UIGestureRecognizerStateChanged;
}

NSArray<UITouch *> *coalesced = [event coalescedTouchesForTouch:touch] ?: @[touch];
NSArray<UITouch *> *predicted = [event predictedTouchesForTouch:touch] ?: @[];
PencilTouchData *data = [[PencilTouchData alloc] initWithCoalescedTouches:coalesced
predictedTouches:predicted
referenceView:self.view
touchType:_trackedTouchType];
[self.pencilDelegate pencilRecognizer:self touchMovedWith:data];
return;
}
}
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (_trackedTouch == nil) {
return;
}

for (UITouch *touch in touches) {
if (touch == _trackedTouch) {
// Flush deferred began if finger was tapped without moving
if (_pendingBeganData) {
self.state = UIGestureRecognizerStateBegan;
[self.pencilDelegate pencilRecognizer:self touchBeganWith:_pendingBeganData];
_pendingBeganData = nil;
}

NSArray<UITouch *> *coalesced = [event coalescedTouchesForTouch:touch] ?: @[touch];
NSArray<UITouch *> *predicted = [event predictedTouchesForTouch:touch] ?: @[];
PencilTouchData *data = [[PencilTouchData alloc] initWithCoalescedTouches:coalesced
predictedTouches:predicted
referenceView:self.view
touchType:_trackedTouchType];
[self.pencilDelegate pencilRecognizer:self touchEndedWith:data];

self.state = UIGestureRecognizerStateEnded;
_trackedTouch = nil;
return;
}
}
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (_trackedTouch == nil) {
return;
}

for (UITouch *touch in touches) {
if (touch == _trackedTouch) {
NSArray<UITouch *> *coalesced = @[touch];
PencilTouchData *data = [[PencilTouchData alloc] initWithCoalescedTouches:coalesced
predictedTouches:@[]
referenceView:self.view
touchType:_trackedTouchType];
[self.pencilDelegate pencilRecognizer:self touchCancelledWith:data];

self.state = UIGestureRecognizerStateCancelled;
_trackedTouch = nil;
return;
}
}
}

// Allow RNGH gesture recognizers to run simultaneously — the native
// recognizer must not block 2-finger zoom/pan on the parent view.
- (BOOL)canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer
{
return NO;
}

- (BOOL)canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer
{
return NO;
}

- (void)reset
{
[super reset];
_trackedTouch = nil;
_trackedTouchType = UITouchTypeDirect;
_pendingBeganData = nil;
}

@end
8 changes: 8 additions & 0 deletions packages/pointer-native-drawing/ios/StylusInputView.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#import <React/RCTViewComponentView.h>

NS_ASSUME_NONNULL_BEGIN

@interface StylusInputView : RCTViewComponentView
@end

NS_ASSUME_NONNULL_END
Loading
Loading