diff --git a/src/video/uikit/SDL_uikitpen.h b/src/video/uikit/SDL_uikitpen.h index c63df2c28..ffcfd0f57 100644 --- a/src/video/uikit/SDL_uikitpen.h +++ b/src/video/uikit/SDL_uikitpen.h @@ -26,12 +26,14 @@ #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); + +#if !defined(SDL_PLATFORM_TVOS) && defined(__IPHONE_13_0) +extern void UIKit_HandlePenHover(SDL_uikitview *view, UIHoverGestureRecognizer *recognizer) API_AVAILABLE(ios(13.0)); +#endif + 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 index bfa62ebc0..6a26e46cc 100644 --- a/src/video/uikit/SDL_uikitpen.m +++ b/src/video/uikit/SDL_uikitpen.m @@ -28,66 +28,162 @@ #include "../../events/SDL_pen_c.h" -SDL_PenID penId; - -typedef struct UIKit_PenHandle -{ - SDL_PenID pen; -} UIKit_PenHandle; +static SDL_PenID apple_pencil_id = 0; bool UIKit_InitPen(SDL_VideoDevice *_this) { return true; } -void UIKit_HandlePenEnter() +// we only have one Apple Pencil at a time, and it must be paired to the iOS device. +// We only know about its existence when it first sends an event, so add an single SDL pen +// device here if we haven't already. +static SDL_PenID UIKit_AddPenIfNecesary() { - 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; + if (!apple_pencil_id) { + SDL_PenInfo info; + SDL_zero(info); + info.capabilities = SDL_PEN_CAPABILITY_PRESSURE | SDL_PEN_CAPABILITY_XTILT | SDL_PEN_CAPABILITY_YTILT; + info.max_tilt = 90.0f; + info.num_buttons = 0; + info.subtype = SDL_PEN_TYPE_PENCIL; - // probably make this better - penId = SDL_AddPenDevice(0, [@"Apple Pencil" UTF8String], &penInfo, calloc(1, sizeof(UIKit_PenHandle))); + if (@available(iOS 17.5, *)) { // need rollAngle method. + info.capabilities |= SDL_PEN_CAPABILITY_ROTATION; + } + + if (@available(ios 16.1, *)) { // need zOffset method. + info.capabilities |= SDL_PEN_CAPABILITY_DISTANCE; + } + + // Apple Pencil and iOS can report when the pencil is being "squeezed" but it's a boolean thing, + // so we can't use it for tangential pressure. + + // There's only ever one Apple Pencil at most, so we just pass a non-zero value for the handle. + apple_pencil_id = SDL_AddPenDevice(0, "Apple Pencil", &info, (void *) (size_t) 0x1); + } + + return apple_pencil_id; } -void UIKit_HandlePenHover(SDL_uikitview *view, CGPoint point) +static void UIKit_HandlePenAxes(SDL_Window *window, NSTimeInterval nstimestamp, float zOffset, const CGPoint *point, float force, + float maximumPossibleForce, float azimuthAngleInView, float altitudeAngle, float rollAngle) { - SDL_SendPenMotion(0, penId, [view getSDLWindow], point.x, point.y); + const SDL_PenID penId = UIKit_AddPenIfNecesary(); + if (penId) { + const Uint64 timestamp = UIKit_GetEventTimestamp(nstimestamp); + const float radians_to_degrees = 180.0f / SDL_PI_F; + + // Normalize force to 0.0f ... 1.0f range. + const float pressure = force / maximumPossibleForce; + + // azimuthAngleInView is in radians, with 0 being the pen's back end pointing due east on the screen when the + // tip is touching the screen, and negative when heading north from there, positive to the south. + // So convert to degrees, 0 being due east, etc. + const float azimuth_angle = azimuthAngleInView * radians_to_degrees; + + // altitudeAngle is in radians, with 0 being the pen laying flat on (parallel to) the device + // screen and PI/2 being it pointing straight up from (perpendicular to) the device screen. + // So convert to degrees, 0 being flat and 90 being straight up. + const float altitude_angle = altitudeAngle * radians_to_degrees; + + // the azimuth_angle goes from -180 to 180 (with abs(angle) moving from 180 to 0, left to right), but SDL wants + // it from -90 (back facing west) to 90 (back facing east). + const float xtilt = (180.0f - SDL_fabsf(azimuth_angle)) - 90.0f; + + // the altitude_angle goes from 0 to 90 regardless of which direction the pen is lifting off the device, but SDL wants + // it from -90 (flat facing north) to 90 (flat facing south). + const float ytilt = (azimuth_angle < 0.0f) ? -(90.0f - altitude_angle) : (90.0f - altitude_angle); + + // rotation is in radians, and only available on a later iOS. + const float rotation = rollAngle * radians_to_degrees; // !!! FIXME: this might need adjustment, I don't have a pencil that supports it. + + SDL_SendPenMotion(timestamp, penId, window, point->x, point->y); + SDL_SendPenAxis(timestamp, penId, window, SDL_PEN_AXIS_PRESSURE, pressure); + SDL_SendPenAxis(timestamp, penId, window, SDL_PEN_AXIS_XTILT, xtilt); + SDL_SendPenAxis(timestamp, penId, window, SDL_PEN_AXIS_YTILT, ytilt); + SDL_SendPenAxis(timestamp, penId, window, SDL_PEN_AXIS_ROTATION, rotation); + SDL_SendPenAxis(timestamp, penId, window, SDL_PEN_AXIS_DISTANCE, zOffset); + } +} + +#if !defined(SDL_PLATFORM_TVOS) && defined(__IPHONE_13_0) +extern void UIKit_HandlePenHover(SDL_uikitview *view, UIHoverGestureRecognizer *recognizer) +{ + float zOffset = 0.0f; + if (@available(iOS 16.1, *)) { + zOffset = (float) [recognizer zOffset]; + } + + float azimuthAngleInView = 0.0f; + if (@available(iOS 16.4, *)) { + azimuthAngleInView = (float) [recognizer azimuthAngleInView:view]; + } + + float altitudeAngle = 0.0f; + if (@available(iOS 16.4, *)) { + altitudeAngle = (float) [recognizer altitudeAngle]; + } + + float rollAngle = 0.0f; + if (@available(iOS 17.5, *)) { + rollAngle = (float) [recognizer rollAngle]; + } + + SDL_Window *window = [view getSDLWindow]; + const CGPoint point = [recognizer locationInView:view]; + + // force is zero here; if you're here, you're not touching. + // !!! FIXME: no timestamp on these...? + UIKit_HandlePenAxes(window, 0, zOffset, &point, 0.0f, 1.0f, azimuthAngleInView, altitudeAngle, rollAngle); +} +#endif + +static void UIKit_HandlePenAxesFromUITouch(SDL_uikitview *view, UITouch *pencil) +{ + float rollAngle = 0.0f; + #if !defined(SDL_PLATFORM_TVOS) + if (@available(iOS 17.5, *)) { + rollAngle = (float) [pencil rollAngle]; + } + #endif + + SDL_Window *window = [view getSDLWindow]; + const CGPoint point = [pencil locationInView:view]; + + // zOffset is zero here; if you're here, you're touching. + UIKit_HandlePenAxes(window, [pencil timestamp], 0.0f, &point, [pencil force], [pencil maximumPossibleForce], [pencil azimuthAngleInView:view], [pencil altitudeAngle], rollAngle); } 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); + UIKit_HandlePenAxesFromUITouch(view, pencil); } void UIKit_HandlePenPress(SDL_uikitview *view, UITouch *pencil) { - SDL_SendPenTouch(UIKit_GetEventTimestamp([pencil timestamp]), penId, [view getSDLWindow], false, true); + const SDL_PenID penId = UIKit_AddPenIfNecesary(); + if (penId) { + UIKit_HandlePenAxesFromUITouch(view, 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; + const SDL_PenID penId = UIKit_AddPenIfNecesary(); + if (penId) { + SDL_SendPenTouch(UIKit_GetEventTimestamp([pencil timestamp]), penId, [view getSDLWindow], false, false); + UIKit_HandlePenAxesFromUITouch(view, pencil); + } } void UIKit_QuitPen(SDL_VideoDevice *_this) { + if (apple_pencil_id) { + SDL_RemovePenDevice(0, apple_pencil_id); + apple_pencil_id = 0; + } } #endif // SDL_VIDEO_DRIVER_UIKIT diff --git a/src/video/uikit/SDL_uikitview.m b/src/video/uikit/SDL_uikitview.m index b5629517e..82d66adef 100644 --- a/src/video/uikit/SDL_uikitview.m +++ b/src/video/uikit/SDL_uikitview.m @@ -261,17 +261,13 @@ extern int SDL_AppleTVRemoteOpenedAsJoystick; { switch (recognizer.state) { case UIGestureRecognizerStateBegan: - UIKit_HandlePenEnter(); - UIKit_HandlePenHover(self, [recognizer locationInView:self]); - break; - case UIGestureRecognizerStateChanged: - UIKit_HandlePenHover(self, [recognizer locationInView:self]); + UIKit_HandlePenHover(self, recognizer); break; case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: - UIKit_HandlePenLeave(); + // we track touches elsewhere, so if a hover "ends" we'll deal with that there. break; default: