From a1ade13f1ef2fe823d2545a4c49b047d4dc690ca Mon Sep 17 00:00:00 2001 From: Sam Lantinga Date: Wed, 19 Nov 2025 10:37:33 -0800 Subject: [PATCH] Enable gamepad events on visionOS Normally the gamepad is used for navigation on visionOS, but when the controller subsystem is enabled we want to receive gamepad input as gamepad events instead. --- src/joystick/apple/SDL_mfijoystick.m | 9 +++ src/video/uikit/SDL_uikitvideo.h | 17 +++-- src/video/uikit/SDL_uikitvideo.m | 78 ++++++++++++++++++++++- src/video/uikit/SDL_uikitviewcontroller.m | 4 ++ src/video/uikit/SDL_uikitwindow.m | 39 +----------- 5 files changed, 101 insertions(+), 46 deletions(-) diff --git a/src/joystick/apple/SDL_mfijoystick.m b/src/joystick/apple/SDL_mfijoystick.m index a7ab051ebe..484891bb1b 100644 --- a/src/joystick/apple/SDL_mfijoystick.m +++ b/src/joystick/apple/SDL_mfijoystick.m @@ -26,6 +26,7 @@ #include "../hidapi/SDL_hidapijoystick_c.h" #include "../usb_ids.h" #include "../../events/SDL_events_c.h" +#include "../../video/uikit/SDL_uikitvideo.h" #include "SDL_mfijoystick_c.h" @@ -790,6 +791,10 @@ static bool IOS_JoystickInit(void) SDL_UnlockJoysticks(); }]; #endif // SDL_JOYSTICK_MFI + +#ifdef SDL_VIDEO_DRIVER_UIKIT + UIKit_SetGameControllerInteraction(true); +#endif } return true; @@ -1581,6 +1586,10 @@ static void IOS_JoystickQuit(void) while (deviceList != NULL) { IOS_RemoveJoystickDevice(deviceList); } + +#ifdef SDL_VIDEO_DRIVER_UIKIT + UIKit_SetGameControllerInteraction(false); +#endif } numjoysticks = 0; diff --git a/src/video/uikit/SDL_uikitvideo.h b/src/video/uikit/SDL_uikitvideo.h index 0f69503b2c..38d41037ce 100644 --- a/src/video/uikit/SDL_uikitvideo.h +++ b/src/video/uikit/SDL_uikitvideo.h @@ -36,19 +36,24 @@ @end #ifdef SDL_PLATFORM_VISIONOS -CGRect UIKit_ComputeViewFrame(SDL_Window *window); +extern CGRect UIKit_ComputeViewFrame(SDL_Window *window); #else -CGRect UIKit_ComputeViewFrame(SDL_Window *window, UIScreen *screen); +extern CGRect UIKit_ComputeViewFrame(SDL_Window *window, UIScreen *screen); #endif +extern API_AVAILABLE(ios(13.0)) UIWindowScene *UIKit_GetActiveWindowScene(void); + +extern void UIKit_SetGameControllerInteraction(bool enabled); +extern void UIKit_SetViewGameControllerInteraction(UIView *view, bool enabled); + #endif // __OBJC__ -bool UIKit_SuspendScreenSaver(SDL_VideoDevice *_this); +extern bool UIKit_SuspendScreenSaver(SDL_VideoDevice *_this); -void UIKit_ForceUpdateHomeIndicator(void); +extern void UIKit_ForceUpdateHomeIndicator(void); -bool UIKit_IsSystemVersionAtLeast(double version); +extern bool UIKit_IsSystemVersionAtLeast(double version); -SDL_SystemTheme UIKit_GetSystemTheme(void); +extern SDL_SystemTheme UIKit_GetSystemTheme(void); #endif // SDL_uikitvideo_h_ diff --git a/src/video/uikit/SDL_uikitvideo.m b/src/video/uikit/SDL_uikitvideo.m index aaa1d5076a..2cca9bc0c2 100644 --- a/src/video/uikit/SDL_uikitvideo.m +++ b/src/video/uikit/SDL_uikitvideo.m @@ -23,6 +23,7 @@ #ifdef SDL_VIDEO_DRIVER_UIKIT #import +#import #include "../SDL_sysvideo.h" #include "../SDL_pixels_c.h" @@ -210,7 +211,8 @@ SDL_SystemTheme UIKit_GetSystemTheme(void) } #ifdef SDL_PLATFORM_VISIONOS -CGRect UIKit_ComputeViewFrame(SDL_Window *window){ +CGRect UIKit_ComputeViewFrame(SDL_Window *window) +{ return CGRectMake(window->x, window->y, window->w, window->h); } #else @@ -251,8 +253,80 @@ CGRect UIKit_ComputeViewFrame(SDL_Window *window, UIScreen *screen) return frame; } +#endif // SDL_PLATFORM_VISIONOS -#endif +UIWindowScene *UIKit_GetActiveWindowScene(void) +{ + if (@available(iOS 13.0, tvOS 13.0, *)) { + NSSet *connectedScenes = [UIApplication sharedApplication].connectedScenes; + + // First, try to find an active foreground scene + for (UIScene *scene in connectedScenes) { + if ([scene isKindOfClass:[UIWindowScene class]]) { + UIWindowScene *windowScene = (UIWindowScene *)scene; + if (windowScene.activationState == UISceneActivationStateForegroundActive) { + return windowScene; + } + } + } + + // If no active scene, return any foreground scene + for (UIScene *scene in connectedScenes) { + if ([scene isKindOfClass:[UIWindowScene class]]) { + UIWindowScene *windowScene = (UIWindowScene *)scene; + if (windowScene.activationState == UISceneActivationStateForegroundInactive) { + return windowScene; + } + } + } + + // Last resort: return first window scene + for (UIScene *scene in connectedScenes) { + if ([scene isKindOfClass:[UIWindowScene class]]) { + return (UIWindowScene *)scene; + } + } + } + + return nil; +} + +void UIKit_SetGameControllerInteraction(bool enabled) +{ + if (@available(iOS 13.0, tvOS 13.0, *)) { + UIWindowScene *scene = UIKit_GetActiveWindowScene(); + if (scene == nil) { + return; + } + + for (UIWindow *window in scene.windows) { + UIKit_SetViewGameControllerInteraction(window, enabled); + } + } +} + +void UIKit_SetViewGameControllerInteraction(UIView *view, bool enabled) +{ +#ifndef SDL_PLATFORM_TVOS + if (@available(iOS 18.0, visionOS 2.0, *)) { + if (enabled) { + GCEventInteraction *interaction = [[GCEventInteraction alloc] init]; + interaction.handledEventTypes = GCUIEventTypeGamepad; + [view addInteraction:interaction]; + } else { + for (id entry in view.interactions) { + if ([entry isKindOfClass:[GCEventInteraction class]]) { + GCEventInteraction *interaction = (GCEventInteraction *)entry; + if (interaction.handledEventTypes == GCUIEventTypeGamepad) { + [view removeInteraction:interaction]; + break; + } + } + } + } + } +#endif // !SDL_PLATFORM_TVOS +} void UIKit_ForceUpdateHomeIndicator(void) { diff --git a/src/video/uikit/SDL_uikitviewcontroller.m b/src/video/uikit/SDL_uikitviewcontroller.m index 6bdefae955..845c0d3686 100644 --- a/src/video/uikit/SDL_uikitviewcontroller.m +++ b/src/video/uikit/SDL_uikitviewcontroller.m @@ -341,6 +341,10 @@ static void SDLCALL SDL_HideHomeIndicatorHintChanged(void *userdata, const char { [super setView:view]; + if (SDL_WasInit(SDL_INIT_JOYSTICK)) { + UIKit_SetViewGameControllerInteraction(view, true); + } + [view addSubview:textField]; if (textFieldFocused) { diff --git a/src/video/uikit/SDL_uikitwindow.m b/src/video/uikit/SDL_uikitwindow.m index 22a863e61f..b16319ef84 100644 --- a/src/video/uikit/SDL_uikitwindow.m +++ b/src/video/uikit/SDL_uikitwindow.m @@ -126,43 +126,6 @@ static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow return true; } -API_AVAILABLE(ios(13.0)) -static UIWindowScene *GetActiveWindowScene(void) -{ - if (@available(iOS 13.0, tvOS 13.0, *)) { - NSSet *connectedScenes = [UIApplication sharedApplication].connectedScenes; - - // First, try to find an active foreground scene - for (UIScene *scene in connectedScenes) { - if ([scene isKindOfClass:[UIWindowScene class]]) { - UIWindowScene *windowScene = (UIWindowScene *)scene; - if (windowScene.activationState == UISceneActivationStateForegroundActive) { - return windowScene; - } - } - } - - // If no active scene, return any foreground scene - for (UIScene *scene in connectedScenes) { - if ([scene isKindOfClass:[UIWindowScene class]]) { - UIWindowScene *windowScene = (UIWindowScene *)scene; - if (windowScene.activationState == UISceneActivationStateForegroundInactive) { - return windowScene; - } - } - } - - // Last resort: return first window scene - for (UIScene *scene in connectedScenes) { - if ([scene isKindOfClass:[UIWindowScene class]]) { - return (UIWindowScene *)scene; - } - } - } - - return nil; -} - bool UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props) { @autoreleasepool { @@ -210,7 +173,7 @@ bool UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Properti UIWindow *uiwindow = nil; if (@available(iOS 13.0, tvOS 13.0, *)) { - UIWindowScene *scene = GetActiveWindowScene(); + UIWindowScene *scene = UIKit_GetActiveWindowScene(); if (scene) { uiwindow = [[UIWindow alloc] initWithWindowScene:scene]; }