SDL: uikit: Initial Apple Pencil support.

From 774e38d073c8fd0abf91570e3f680b5a4eb5b297 Mon Sep 17 00:00:00 2001
From: Salman Alshamrani <[EMAIL REDACTED]>
Date: Tue, 17 Dec 2024 08:59:49 -0500
Subject: [PATCH] uikit: Initial Apple Pencil support.

Reference Issue #9911.
Reference Issue #10516.
---
 src/video/uikit/SDL_uikitpen.h  |  37 +++++
 src/video/uikit/SDL_uikitpen.m  |  93 ++++++++++++
 src/video/uikit/SDL_uikitview.h |   7 +
 src/video/uikit/SDL_uikitview.m | 254 ++++++++++++++++++++++----------
 4 files changed, 317 insertions(+), 74 deletions(-)
 create mode 100644 src/video/uikit/SDL_uikitpen.h
 create mode 100644 src/video/uikit/SDL_uikitpen.m

diff --git a/src/video/uikit/SDL_uikitpen.h b/src/video/uikit/SDL_uikitpen.h
new file mode 100644
index 0000000000000..c63df2c2857b6
--- /dev/null
+++ b/src/video/uikit/SDL_uikitpen.h
@@ -0,0 +1,37 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifndef SDL_uikitpen_h_
+#define SDL_uikitpen_h_
+
+#include "SDL_uikitvideo.h"
+#include "SDL_uikitwindow.h"
+
+extern bool UIKit_InitPen(SDL_VideoDevice *_this);
+extern void UIKit_HandlePenEnter();
+extern void UIKit_HandlePenLeave();
+extern void UIKit_HandlePenHover(SDL_uikitview *view, CGPoint point);
+extern void UIKit_HandlePenMotion(SDL_uikitview *view, UITouch *pencil);
+extern void UIKit_HandlePenPress(SDL_uikitview *view, UITouch *pencil);
+extern void UIKit_HandlePenRelease(SDL_uikitview *view, UITouch *pencil);
+extern void UIKit_QuitPen(SDL_VideoDevice *_this);
+
+#endif // SDL_uikitpen_h_
diff --git a/src/video/uikit/SDL_uikitpen.m b/src/video/uikit/SDL_uikitpen.m
new file mode 100644
index 0000000000000..bfa62ebc00679
--- /dev/null
+++ b/src/video/uikit/SDL_uikitpen.m
@@ -0,0 +1,93 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#ifdef SDL_VIDEO_DRIVER_UIKIT
+
+#include "SDL_uikitevents.h"
+#include "SDL_uikitpen.h"
+#include "SDL_uikitwindow.h"
+
+#include "../../events/SDL_pen_c.h"
+
+SDL_PenID penId;
+
+typedef struct UIKit_PenHandle
+{
+    SDL_PenID pen;
+} UIKit_PenHandle;
+
+bool UIKit_InitPen(SDL_VideoDevice *_this)
+{
+    return true;
+}
+
+void UIKit_HandlePenEnter()
+{
+    SDL_PenInfo penInfo;
+    SDL_zero(penInfo);
+    penInfo.capabilities = SDL_PEN_CAPABILITY_PRESSURE | SDL_PEN_CAPABILITY_ROTATION | SDL_PEN_CAPABILITY_XTILT | SDL_PEN_CAPABILITY_YTILT | SDL_PEN_CAPABILITY_TANGENTIAL_PRESSURE;
+    penInfo.max_tilt = 90.0f;
+    penInfo.num_buttons = 0;
+    penInfo.subtype = SDL_PEN_TYPE_PENCIL;
+
+    // probably make this better
+    penId = SDL_AddPenDevice(0, [@"Apple Pencil" UTF8String], &penInfo, calloc(1, sizeof(UIKit_PenHandle)));
+}
+
+void UIKit_HandlePenHover(SDL_uikitview *view, CGPoint point)
+{
+    SDL_SendPenMotion(0, penId, [view getSDLWindow], point.x, point.y);
+}
+
+void UIKit_HandlePenMotion(SDL_uikitview *view, UITouch *pencil)
+{
+    CGPoint point = [pencil locationInView:view];
+    SDL_SendPenMotion(UIKit_GetEventTimestamp([pencil timestamp]), penId, [view getSDLWindow], point.x, point.y);
+    SDL_SendPenAxis(UIKit_GetEventTimestamp([pencil timestamp]), penId, [view getSDLWindow], SDL_PEN_AXIS_PRESSURE, [pencil force] / [pencil maximumPossibleForce]);
+    NSLog(@"ALTITUDE: %f", [pencil altitudeAngle]);
+    NSLog(@"AZIMUTH VECTOR: %@", [NSValue valueWithCGVector: [pencil azimuthUnitVectorInView:view]]);
+    NSLog(@"AZIMUTH ANGLE: %f", [pencil azimuthAngleInView:view]);
+    // hold it
+    //    SDL_SendPenAxis(0, penId, [view getSDLWindow], SDL_PEN_AXIS_XTILT, [pencil altitudeAngle] / M_PI);
+}
+
+void UIKit_HandlePenPress(SDL_uikitview *view, UITouch *pencil)
+{
+    SDL_SendPenTouch(UIKit_GetEventTimestamp([pencil timestamp]), penId, [view getSDLWindow], false, true);
+}
+
+void UIKit_HandlePenRelease(SDL_uikitview *view, UITouch *pencil)
+{
+    SDL_SendPenTouch(UIKit_GetEventTimestamp([pencil timestamp]), penId, [view getSDLWindow], false, false);
+}
+
+void UIKit_HandlePenLeave()
+{
+    SDL_RemovePenDevice(0, penId);
+    penId = 0;
+}
+
+void UIKit_QuitPen(SDL_VideoDevice *_this)
+{
+}
+
+#endif // SDL_VIDEO_DRIVER_UIKIT
diff --git a/src/video/uikit/SDL_uikitview.h b/src/video/uikit/SDL_uikitview.h
index 6169ccfa022b4..5d2121428712c 100644
--- a/src/video/uikit/SDL_uikitview.h
+++ b/src/video/uikit/SDL_uikitview.h
@@ -34,9 +34,16 @@
 - (void)setSDLWindow:(SDL_Window *)window;
 - (SDL_Window *)getSDLWindow;
 
+#if defined(__IPHONE_13_0)
+- (void)pencilHovering:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0));
+#endif
+
 #if !defined(SDL_PLATFORM_TVOS) && defined(__IPHONE_13_4)
 - (UIPointerRegion *)pointerInteraction:(UIPointerInteraction *)interaction regionForRequest:(UIPointerRegionRequest *)request defaultRegion:(UIPointerRegion *)defaultRegion API_AVAILABLE(ios(13.4));
 - (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction styleForRegion:(UIPointerRegion *)region API_AVAILABLE(ios(13.4));
+- (void)indirectPointerHovering:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.4));
+- (void)updateIndirectPointerFromTouch:(UITouch *)touch;
+- (void)updateIndirectPointerButtonState:(UITouch *)touch fromEvent:(UIEvent *)event;
 #endif
 
 - (CGPoint)touchLocation:(UITouch *)touch shouldNormalize:(BOOL)normalize;
diff --git a/src/video/uikit/SDL_uikitview.m b/src/video/uikit/SDL_uikitview.m
index 215f84712d835..99ebc774d63f0 100644
--- a/src/video/uikit/SDL_uikitview.m
+++ b/src/video/uikit/SDL_uikitview.m
@@ -31,6 +31,7 @@
 #include "SDL_uikitappdelegate.h"
 #include "SDL_uikitevents.h"
 #include "SDL_uikitmodes.h"
+#include "SDL_uikitpen.h"
 #include "SDL_uikitwindow.h"
 
 // The maximum number of mouse buttons we support
@@ -47,6 +48,10 @@ @implementation SDL_uikitview
 
     SDL_TouchID directTouchId;
     SDL_TouchID indirectTouchId;
+
+#if defined(__IPHONE_13_4)
+    UIPointerInteraction *indirectPointerInteraction API_AVAILABLE(ios(13.4));
+#endif
 }
 
 - (instancetype)initWithFrame:(CGRect)frame
@@ -81,10 +86,23 @@ - (instancetype)initWithFrame:(CGRect)frame
         self.multipleTouchEnabled = YES;
         SDL_AddTouch(directTouchId, SDL_TOUCH_DEVICE_DIRECT, "");
 #endif
+        
+#if defined(__IPHONE_13_0)
+        if (@available(iOS 13.0, *)) {
+            UIHoverGestureRecognizer *pencilRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(pencilHovering:)];
+            pencilRecognizer.allowedTouchTypes = @[@(UITouchTypePencil)];
+            [self addGestureRecognizer:pencilRecognizer];
+        }
+#endif
 
 #if !defined(SDL_PLATFORM_TVOS) && defined(__IPHONE_13_4)
         if (@available(iOS 13.4, *)) {
-            [self addInteraction:[[UIPointerInteraction alloc] initWithDelegate:self]];
+            indirectPointerInteraction = [[UIPointerInteraction alloc] initWithDelegate:self];
+            [self addInteraction:indirectPointerInteraction];
+
+            UIHoverGestureRecognizer *indirectPointerRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(indirectPointerHovering:)];
+            indirectPointerRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
+            [self addGestureRecognizer:indirectPointerRecognizer];
         }
 #endif
     }
@@ -156,15 +174,6 @@ - (SDL_Window *)getSDLWindow
 #if !defined(SDL_PLATFORM_TVOS) && defined(__IPHONE_13_4)
 - (UIPointerRegion *)pointerInteraction:(UIPointerInteraction *)interaction regionForRequest:(UIPointerRegionRequest *)request defaultRegion:(UIPointerRegion *)defaultRegion API_AVAILABLE(ios(13.4))
 {
-    if (request != nil && !SDL_GCMouseRelativeMode()) {
-        CGPoint origin = self.bounds.origin;
-        CGPoint point = request.location;
-
-        point.x -= origin.x;
-        point.y -= origin.y;
-
-        SDL_SendMouseMotion(0, sdlwindow, SDL_GLOBAL_MOUSE_ID, false, point.x, point.y);
-    }
     return [UIPointerRegion regionWithRect:self.bounds identifier:nil];
 }
 
@@ -176,8 +185,115 @@ - (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction style
         return [UIPointerStyle hiddenPointerStyle];
     }
 }
+
+- (void)indirectPointerHovering:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.4))
+{
+    switch (recognizer.state) {
+        case UIGestureRecognizerStateBegan:
+        case UIGestureRecognizerStateChanged:
+        {
+            CGPoint point = [recognizer locationInView:self];
+            SDL_SendMouseMotion(0, sdlwindow, SDL_GLOBAL_MOUSE_ID, false, point.x, point.y);
+            break;
+        }
+
+        default:
+            break;
+    }
+}
+
+- (void)indirectPointerMoving:(UITouch *)touch API_AVAILABLE(ios(13.4))
+{
+    CGPoint locationInView = [self touchLocation:touch shouldNormalize:NO];
+    SDL_SendMouseMotion(0, sdlwindow, SDL_GLOBAL_MOUSE_ID, false, locationInView.x, locationInView.y);
+}
+
+- (void)indirectPointerPressed:(UITouch *)touch fromEvent:(UIEvent *)event API_AVAILABLE(ios(13.4))
+{
+    if (!SDL_HasMouse()) {
+        int i;
+
+        for (i = 1; i <= MAX_MOUSE_BUTTONS; ++i) {
+            if (event.buttonMask & SDL_BUTTON_MASK(i)) {
+                Uint8 button;
+
+                switch (i) {
+                case 1:
+                    button = SDL_BUTTON_LEFT;
+                    break;
+                case 2:
+                    button = SDL_BUTTON_RIGHT;
+                    break;
+                case 3:
+                    button = SDL_BUTTON_MIDDLE;
+                    break;
+                default:
+                    button = (Uint8)i;
+                    break;
+                }
+                SDL_SendMouseButton(UIKit_GetEventTimestamp([touch timestamp]), sdlwindow, SDL_GLOBAL_MOUSE_ID, button, true);
+            }
+        }
+    }
+}
+
+- (void)indirectPointerReleased:(UITouch *)touch fromEvent:(UIEvent *)event API_AVAILABLE(ios(13.4))
+{
+    if (!SDL_HasMouse()) {
+        int i;
+        SDL_MouseButtonFlags buttons = SDL_GetMouseState(NULL, NULL);
+
+        for (i = 0; i < MAX_MOUSE_BUTTONS; ++i) {
+            if (buttons & SDL_BUTTON_MASK(i)) {
+                SDL_SendMouseButton(UIKit_GetEventTimestamp([touch timestamp]), sdlwindow, SDL_GLOBAL_MOUSE_ID, (Uint8)i, false);
+            }
+        }
+    }
+}
+
 #endif // !defined(SDL_PLATFORM_TVOS) && __IPHONE_13_4
 
+#if defined(__IPHONE_13_0)
+
+- (void)pencilHovering:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0))
+{
+    switch (recognizer.state) {
+        case UIGestureRecognizerStateBegan:
+            UIKit_HandlePenEnter();
+            UIKit_HandlePenHover(self, [recognizer locationInView:self]);
+            break;
+
+        case UIGestureRecognizerStateChanged:
+            UIKit_HandlePenHover(self, [recognizer locationInView:self]);
+            break;
+
+        case UIGestureRecognizerStateEnded:
+        case UIGestureRecognizerStateCancelled:
+            UIKit_HandlePenLeave();
+            break;
+
+        default:
+            break;
+    }
+}
+
+- (void)pencilMoving:(UITouch *)touch
+{
+    UIKit_HandlePenMotion(self, touch);
+}
+
+- (void)pencilPressed:(UITouch *)touch
+{
+    UIKit_HandlePenPress(self, touch);
+}
+
+- (void)pencilReleased:(UITouch *)touch
+{
+    UIKit_HandlePenRelease(self, touch);
+}
+
+#endif // defined(__IPHONE_13_0)
+
 - (SDL_TouchDeviceType)touchTypeForTouch:(UITouch *)touch
 {
 #ifdef __IPHONE_9_0
@@ -231,34 +347,19 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
     for (UITouch *touch in touches) {
         BOOL handled = NO;
 
+#if defined(__IPHONE_13_0)
+        if (@available(iOS 13.0, *)) {
+            if (touch.type == UITouchTypePencil) {
+                [self pencilPressed:touch];
+                continue;
+            }
+        }
+#endif
+        
 #if !defined(SDL_PLATFORM_TVOS) && defined(__IPHONE_13_4)
         if (@available(iOS 13.4, *)) {
             if (touch.type == UITouchTypeIndirectPointer) {
-                if (!SDL_HasMouse()) {
-                    int i;
-
-                    for (i = 1; i <= MAX_MOUSE_BUTTONS; ++i) {
-                        if (event.buttonMask & SDL_BUTTON_MASK(i)) {
-                            Uint8 button;
-
-                            switch (i) {
-                            case 1:
-                                button = SDL_BUTTON_LEFT;
-                                break;
-                            case 2:
-                                button = SDL_BUTTON_RIGHT;
-                                break;
-                            case 3:
-                                button = SDL_BUTTON_MIDDLE;
-                                break;
-                            default:
-                                button = (Uint8)i;
-                                break;
-                            }
-                            SDL_SendMouseButton(UIKit_GetEventTimestamp([event timestamp]), sdlwindow, SDL_GLOBAL_MOUSE_ID, button, true);
-                        }
-                    }
-                }
+                [self indirectPointerPressed:touch fromEvent:event];
                 handled = YES;
             }
         }
@@ -286,40 +387,38 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
 {
     for (UITouch *touch in touches) {
         BOOL handled = NO;
-
+        
+#if defined(__IPHONE_13_0)
+        if (@available(iOS 13.0, *)) {
+            if (touch.type == UITouchTypePencil) {
+                [self pencilReleased:touch];
+                continue;
+            }
+        }
+#endif
+        
 #if !defined(SDL_PLATFORM_TVOS) && defined(__IPHONE_13_4)
         if (@available(iOS 13.4, *)) {
             if (touch.type == UITouchTypeIndirectPointer) {
-                if (!SDL_HasMouse()) {
-                    int i;
-                    SDL_MouseButtonFlags buttons = SDL_GetMouseState(NULL, NULL);
-
-                    for (i = 0; i < MAX_MOUSE_BUTTONS; ++i) {
-                        if (buttons & SDL_BUTTON_MASK(i)) {
-                            SDL_SendMouseButton(UIKit_GetEventTimestamp([event timestamp]), sdlwindow, SDL_GLOBAL_MOUSE_ID, (Uint8)i, false);
-                        }
-                    }
-                }
-                handled = YES;
+                [self indirectPointerReleased:touch fromEvent:event];
+                continue;
             }
         }
 #endif
-        if (!handled) {
-            SDL_TouchDeviceType touchType = [self touchTypeForTouch:touch];
-            SDL_TouchID touchId = [self touchIdForType:touchType];
-            float pressure = [self pressureForTouch:touch];
+        SDL_TouchDeviceType touchType = [self touchTypeForTouch:touch];
+        SDL_TouchID touchId = [self touchIdForType:touchType];
+        float pressure = [self pressureForTouch:touch];
 
-            if (SDL_AddTouch(touchId, touchType, "") < 0) {
-                continue;
-            }
+        if (SDL_AddTouch(touchId, touchType, "") < 0) {
+            continue;
+        }
 
-            // FIXME, need to send: int clicks = (int) touch.tapCount; ?
+        // FIXME, need to send: int clicks = (int) touch.tapCount; ?
 
-            CGPoint locationInView = [self touchLocation:touch shouldNormalize:YES];
-            SDL_SendTouch(UIKit_GetEventTimestamp([event timestamp]),
-                          touchId, (SDL_FingerID)(uintptr_t)touch, sdlwindow,
-                          false, locationInView.x, locationInView.y, pressure);
-        }
+        CGPoint locationInView = [self touchLocation:touch shouldNormalize:YES];
+        SDL_SendTouch(UIKit_GetEventTimestamp([event timestamp]),
+                      touchId, (SDL_FingerID)(uintptr_t)touch, sdlwindow,
+                      false, locationInView.x, locationInView.y, pressure);
     }
 }
 
@@ -332,29 +431,36 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
 {
     for (UITouch *touch in touches) {
         BOOL handled = NO;
+        
+#if defined(__IPHONE_13_0)
+        if (@available(iOS 13.0, *)) {
+            if (touch.type == UITouchTypePencil) {
+                [self pencilMoving:touch];
+                continue;
+            }
+        }
+#endif
 
 #if !defined(SDL_PLATFORM_TVOS) && defined(__IPHONE_13_4)
         if (@available(iOS 13.4, *)) {
             if (touch.type == UITouchTypeIndirectPointer) {
-                // Already handled in pointerInteraction callback
-                handled = YES;
+                [self indirectPointerMoving:touch];
+                continue;
             }
         }
 #endif
-        if (!handled) {
-            SDL_TouchDeviceType touchType = [self touchTypeForTouch:touch];
-            SDL_TouchID touchId = [self touchIdForType:touchType];
-            float pressure = [self pressureForTouch:touch];
+        SDL_TouchDeviceType touchType = [self touchTypeForTouch:touch];
+        SDL_TouchID touchId = [self touchIdForType:touchType];
+        float pressure = [self pressureForTouch:touch];
 
-            if (SDL_AddTouch(touchId, touchType, "") < 0) {
-                continue;
-            }
-
-            CGPoint locationInView = [self touchLocation:touch shouldNormalize:YES];
-            SDL_SendTouchMotion(UIKit_GetEventTimestamp([event timestamp]),
-                                touchId, (SDL_FingerID)(uintptr_t)touch, sdlwindow,
-                                locationInView.x, locationInView.y, pressure);
+        if (SDL_AddTouch(touchId, touchType, "") < 0) {
+            continue;
         }
+
+        CGPoint locationInView = [self touchLocation:touch shouldNormalize:YES];
+        SDL_SendTouchMotion(UIKit_GetEventTimestamp([event timestamp]),
+                            touchId, (SDL_FingerID)(uintptr_t)touch, sdlwindow,
+                            locationInView.x, locationInView.y, pressure);
     }
 }