From 5a079a2d114f96d4847d1ee305d5b7c16eeec50e Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 27 Dec 2025 12:03:39 -0800 Subject: Initial commit --- .../src/video/uikit/SDL_uikitappdelegate.h | 45 ++ .../src/video/uikit/SDL_uikitappdelegate.m | 519 +++++++++++++++ .../SDL-3.2.8/src/video/uikit/SDL_uikitclipboard.h | 33 + .../SDL-3.2.8/src/video/uikit/SDL_uikitclipboard.m | 105 +++ .../SDL-3.2.8/src/video/uikit/SDL_uikitevents.h | 40 ++ .../SDL-3.2.8/src/video/uikit/SDL_uikitevents.m | 461 +++++++++++++ .../src/video/uikit/SDL_uikitmessagebox.h | 28 + .../src/video/uikit/SDL_uikitmessagebox.m | 154 +++++ .../SDL-3.2.8/src/video/uikit/SDL_uikitmetalview.h | 54 ++ .../SDL-3.2.8/src/video/uikit/SDL_uikitmetalview.m | 140 ++++ contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmodes.h | 67 ++ contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmodes.m | 543 +++++++++++++++ .../SDL-3.2.8/src/video/uikit/SDL_uikitopengles.h | 40 ++ .../SDL-3.2.8/src/video/uikit/SDL_uikitopengles.m | 221 +++++++ .../src/video/uikit/SDL_uikitopenglview.h | 62 ++ .../src/video/uikit/SDL_uikitopenglview.m | 377 +++++++++++ contrib/SDL-3.2.8/src/video/uikit/SDL_uikitpen.h | 39 ++ contrib/SDL-3.2.8/src/video/uikit/SDL_uikitpen.m | 214 ++++++ contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvideo.h | 52 ++ contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvideo.m | 312 +++++++++ contrib/SDL-3.2.8/src/video/uikit/SDL_uikitview.h | 52 ++ contrib/SDL-3.2.8/src/video/uikit/SDL_uikitview.m | 587 ++++++++++++++++ .../src/video/uikit/SDL_uikitviewcontroller.h | 96 +++ .../src/video/uikit/SDL_uikitviewcontroller.m | 736 +++++++++++++++++++++ .../SDL-3.2.8/src/video/uikit/SDL_uikitvulkan.h | 52 ++ .../SDL-3.2.8/src/video/uikit/SDL_uikitvulkan.m | 265 ++++++++ .../SDL-3.2.8/src/video/uikit/SDL_uikitwindow.h | 56 ++ .../SDL-3.2.8/src/video/uikit/SDL_uikitwindow.m | 471 +++++++++++++ 28 files changed, 5821 insertions(+) create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitappdelegate.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitappdelegate.m create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitclipboard.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitclipboard.m create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitevents.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitevents.m create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmessagebox.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmessagebox.m create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmetalview.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmetalview.m create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmodes.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmodes.m create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopengles.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopengles.m create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopenglview.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopenglview.m create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitpen.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitpen.m create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvideo.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvideo.m create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitview.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitview.m create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitviewcontroller.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitviewcontroller.m create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvulkan.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvulkan.m create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitwindow.h create mode 100644 contrib/SDL-3.2.8/src/video/uikit/SDL_uikitwindow.m (limited to 'contrib/SDL-3.2.8/src/video/uikit') diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitappdelegate.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitappdelegate.h new file mode 100644 index 0000000..77ccbfd --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitappdelegate.h @@ -0,0 +1,45 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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. +*/ + +#import + +@interface SDLLaunchScreenController : UIViewController + +- (instancetype)init; +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil; +- (void)loadView; + +@end + +@interface SDLUIKitDelegate : NSObject + ++ (id)sharedAppDelegate; ++ (NSString *)getAppDelegateClassName; + +- (void)hideLaunchScreen; + +/* This property is marked as optional, and is only intended to be used when + * the app's UI is storyboard-based. SDL is not storyboard-based, however + * several major third-party ad APIs (e.g. Google admob) incorrectly assume this + * property always exists, and will crash if it doesn't. */ +@property(nonatomic) UIWindow *window; + +@end diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitappdelegate.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitappdelegate.m new file mode 100644 index 0000000..6a37e51 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitappdelegate.m @@ -0,0 +1,519 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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_sysvideo.h" + +#import "SDL_uikitappdelegate.h" +#import "SDL_uikitmodes.h" +#import "SDL_uikitwindow.h" + +#include "../../events/SDL_events_c.h" + +#ifdef main +#undef main +#endif + +static SDL_main_func forward_main; +static int forward_argc; +static char **forward_argv; +static int exit_status; + +int SDL_RunApp(int argc, char* argv[], SDL_main_func mainFunction, void * reserved) +{ + int i; + + // store arguments + /* Note that we need to be careful about how we allocate/free memory here. + * If the application calls SDL_SetMemoryFunctions(), we can't rely on + * SDL_free() to use the same allocator after SDL_main() returns. + */ + forward_main = mainFunction; + forward_argc = argc; + forward_argv = (char **)malloc((argc + 1) * sizeof(char *)); // This should NOT be SDL_malloc() + for (i = 0; i < argc; i++) { + forward_argv[i] = malloc((strlen(argv[i]) + 1) * sizeof(char)); // This should NOT be SDL_malloc() + strcpy(forward_argv[i], argv[i]); + } + forward_argv[i] = NULL; + + // Give over control to run loop, SDLUIKitDelegate will handle most things from here + @autoreleasepool { + UIApplicationMain(argc, argv, nil, [SDLUIKitDelegate getAppDelegateClassName]); + } + + // free the memory we used to hold copies of argc and argv + for (i = 0; i < forward_argc; i++) { + free(forward_argv[i]); // This should NOT be SDL_free() + } + free(forward_argv); // This should NOT be SDL_free() + + return exit_status; +} + +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) +// Load a launch image using the old UILaunchImageFile-era naming rules. +static UIImage *SDL_LoadLaunchImageNamed(NSString *name, int screenh) +{ + UIInterfaceOrientation curorient = [UIApplication sharedApplication].statusBarOrientation; + UIUserInterfaceIdiom idiom = [UIDevice currentDevice].userInterfaceIdiom; + UIImage *image = nil; + + if (idiom == UIUserInterfaceIdiomPhone && screenh == 568) { + // The image name for the iPhone 5 uses its height as a suffix. + image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-568h", name]]; + } else if (idiom == UIUserInterfaceIdiomPad) { + // iPad apps can launch in any orientation. + if (UIInterfaceOrientationIsLandscape(curorient)) { + if (curorient == UIInterfaceOrientationLandscapeLeft) { + image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-LandscapeLeft", name]]; + } else { + image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-LandscapeRight", name]]; + } + if (!image) { + image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-Landscape", name]]; + } + } else { + if (curorient == UIInterfaceOrientationPortraitUpsideDown) { + image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-PortraitUpsideDown", name]]; + } + if (!image) { + image = [UIImage imageNamed:[NSString stringWithFormat:@"%@-Portrait", name]]; + } + } + } + + if (!image) { + image = [UIImage imageNamed:name]; + } + + return image; +} + +@interface SDLLaunchStoryboardViewController : UIViewController +@property(nonatomic, strong) UIViewController *storyboardViewController; +- (instancetype)initWithStoryboardViewController:(UIViewController *)storyboardViewController; +@end + +@implementation SDLLaunchStoryboardViewController + +- (instancetype)initWithStoryboardViewController:(UIViewController *)storyboardViewController +{ + self = [super init]; + self.storyboardViewController = storyboardViewController; + return self; +} + +- (void)viewDidLoad +{ + [super viewDidLoad]; + + [self addChildViewController:self.storyboardViewController]; + [self.view addSubview:self.storyboardViewController.view]; + self.storyboardViewController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.storyboardViewController.view.frame = self.view.bounds; + [self.storyboardViewController didMoveToParentViewController:self]; + +#ifndef SDL_PLATFORM_VISIONOS + UIApplication.sharedApplication.statusBarHidden = self.prefersStatusBarHidden; + UIApplication.sharedApplication.statusBarStyle = self.preferredStatusBarStyle; +#endif +} + +- (BOOL)prefersStatusBarHidden +{ + return [[NSBundle.mainBundle objectForInfoDictionaryKey:@"UIStatusBarHidden"] boolValue]; +} + +- (UIStatusBarStyle)preferredStatusBarStyle +{ + NSString *statusBarStyle = [NSBundle.mainBundle objectForInfoDictionaryKey:@"UIStatusBarStyle"]; + if ([statusBarStyle isEqualToString:@"UIStatusBarStyleLightContent"]) { + return UIStatusBarStyleLightContent; + } + if (@available(iOS 13.0, *)) { + if ([statusBarStyle isEqualToString:@"UIStatusBarStyleDarkContent"]) { + return UIStatusBarStyleDarkContent; + } + } + return UIStatusBarStyleDefault; +} + +@end +#endif // !SDL_PLATFORM_TVOS + +@interface SDLLaunchScreenController () + +#ifndef SDL_PLATFORM_TVOS +- (NSUInteger)supportedInterfaceOrientations; +#endif + +@end + +@implementation SDLLaunchScreenController + +- (instancetype)init +{ + return [self initWithNibName:nil bundle:[NSBundle mainBundle]]; +} + +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + if (!(self = [super initWithNibName:nil bundle:nil])) { + return nil; + } + + NSString *screenname = nibNameOrNil; + NSBundle *bundle = nibBundleOrNil; + + // A launch screen may not exist. Fall back to launch images in that case. + if (screenname) { + @try { + self.view = [bundle loadNibNamed:screenname owner:self options:nil][0]; + } + @catch (NSException *exception) { + /* If a launch screen name is specified but it fails to load, iOS + * displays a blank screen rather than falling back to an image. */ + return nil; + } + } + + if (!self.view) { + NSArray *launchimages = [bundle objectForInfoDictionaryKey:@"UILaunchImages"]; + NSString *imagename = nil; + UIImage *image = nil; + +#ifdef SDL_PLATFORM_VISIONOS + int screenw = SDL_XR_SCREENWIDTH; + int screenh = SDL_XR_SCREENHEIGHT; +#else + int screenw = (int)([UIScreen mainScreen].bounds.size.width + 0.5); + int screenh = (int)([UIScreen mainScreen].bounds.size.height + 0.5); +#endif + + + +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) + UIInterfaceOrientation curorient = [UIApplication sharedApplication].statusBarOrientation; + + // We always want portrait-oriented size, to match UILaunchImageSize. + if (screenw > screenh) { + int width = screenw; + screenw = screenh; + screenh = width; + } +#endif + + // Xcode 5 introduced a dictionary of launch images in Info.plist. + if (launchimages) { + for (NSDictionary *dict in launchimages) { + NSString *minversion = dict[@"UILaunchImageMinimumOSVersion"]; + NSString *sizestring = dict[@"UILaunchImageSize"]; + + // Ignore this image if the current version is too low. + if (minversion && !UIKit_IsSystemVersionAtLeast(minversion.doubleValue)) { + continue; + } + + // Ignore this image if the size doesn't match. + if (sizestring) { + CGSize size = CGSizeFromString(sizestring); + if ((int)(size.width + 0.5) != screenw || (int)(size.height + 0.5) != screenh) { + continue; + } + } + +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) + UIInterfaceOrientationMask orientmask = UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown; + NSString *orientstring = dict[@"UILaunchImageOrientation"]; + + if (orientstring) { + if ([orientstring isEqualToString:@"PortraitUpsideDown"]) { + orientmask = UIInterfaceOrientationMaskPortraitUpsideDown; + } else if ([orientstring isEqualToString:@"Landscape"]) { + orientmask = UIInterfaceOrientationMaskLandscape; + } else if ([orientstring isEqualToString:@"LandscapeLeft"]) { + orientmask = UIInterfaceOrientationMaskLandscapeLeft; + } else if ([orientstring isEqualToString:@"LandscapeRight"]) { + orientmask = UIInterfaceOrientationMaskLandscapeRight; + } + } + + // Ignore this image if the orientation doesn't match. + if ((orientmask & (1 << curorient)) == 0) { + continue; + } +#endif + + imagename = dict[@"UILaunchImageName"]; + } + + if (imagename) { + image = [UIImage imageNamed:imagename]; + } + } +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) + else { + imagename = [bundle objectForInfoDictionaryKey:@"UILaunchImageFile"]; + + if (imagename) { + image = SDL_LoadLaunchImageNamed(imagename, screenh); + } + + if (!image) { + image = SDL_LoadLaunchImageNamed(@"Default", screenh); + } + } +#endif + + if (image) { +#ifdef SDL_PLATFORM_VISIONOS + CGRect viewFrame = CGRectMake(0, 0, screenw, screenh); +#else + CGRect viewFrame = [UIScreen mainScreen].bounds; +#endif + UIImageView *view = [[UIImageView alloc] initWithFrame:viewFrame]; + UIImageOrientation imageorient = UIImageOrientationUp; + +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) + // Bugs observed / workaround tested in iOS 8.3. + if (UIInterfaceOrientationIsLandscape(curorient)) { + if (image.size.width < image.size.height) { + /* On iOS 8, portrait launch images displayed in forced- + * landscape mode (e.g. a standard Default.png on an iPhone + * when Info.plist only supports landscape orientations) need + * to be rotated to display in the expected orientation. */ + if (curorient == UIInterfaceOrientationLandscapeLeft) { + imageorient = UIImageOrientationRight; + } else if (curorient == UIInterfaceOrientationLandscapeRight) { + imageorient = UIImageOrientationLeft; + } + } + } +#endif + + // Create the properly oriented image. + view.image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:imageorient]; + + self.view = view; + } + } + + return self; +} + +- (void)loadView +{ + // Do nothing. +} + +#ifndef SDL_PLATFORM_TVOS +- (BOOL)shouldAutorotate +{ + // If YES, the launch image will be incorrectly rotated in some cases. + return NO; +} + +- (NSUInteger)supportedInterfaceOrientations +{ + /* We keep the supported orientations unrestricted to avoid the case where + * there are no common orientations between the ones set in Info.plist and + * the ones set here (it will cause an exception in that case.) */ + return UIInterfaceOrientationMaskAll; +} +#endif // !SDL_PLATFORM_TVOS + +@end + +@implementation SDLUIKitDelegate +{ + UIWindow *launchWindow; +} + +// convenience method ++ (id)sharedAppDelegate +{ + /* the delegate is set in UIApplicationMain(), which is guaranteed to be + * called before this method */ + return [UIApplication sharedApplication].delegate; +} + ++ (NSString *)getAppDelegateClassName +{ + /* subclassing notice: when you subclass this appdelegate, make sure to add + * a category to override this method and return the actual name of the + * delegate */ + return @"SDLUIKitDelegate"; +} + +- (void)hideLaunchScreen +{ + UIWindow *window = launchWindow; + + if (!window || window.hidden) { + return; + } + + launchWindow = nil; + + // Do a nice animated fade-out (roughly matches the real launch behavior.) + [UIView animateWithDuration:0.2 + animations:^{ + window.alpha = 0.0; + } + completion:^(BOOL finished) { + window.hidden = YES; + UIKit_ForceUpdateHomeIndicator(); // Wait for launch screen to hide so settings are applied to the actual view controller. + }]; +} + +- (void)postFinishLaunch +{ + /* Hide the launch screen the next time the run loop is run. SDL apps will + * have a chance to load resources while the launch screen is still up. */ + [self performSelector:@selector(hideLaunchScreen) withObject:nil afterDelay:0.0]; + + // run the user's application, passing argc and argv + SDL_SetiOSEventPump(true); + exit_status = forward_main(forward_argc, forward_argv); + SDL_SetiOSEventPump(false); + + if (launchWindow) { + launchWindow.hidden = YES; + launchWindow = nil; + } + + // exit, passing the return status from the user's application + /* We don't actually exit to support applications that do setup in their + * main function and then allow the Cocoa event loop to run. */ + // exit(exit_status); +} + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + NSBundle *bundle = [NSBundle mainBundle]; + +#ifdef SDL_IPHONE_LAUNCHSCREEN + /* The normal launch screen is displayed until didFinishLaunching returns, + * but SDL_main is called after that happens and there may be a noticeable + * delay between the start of SDL_main and when the first real frame is + * displayed (e.g. if resources are loaded before SDL_GL_SwapWindow is + * called), so we show the launch screen programmatically until the first + * time events are pumped. */ + UIViewController *vc = nil; + NSString *screenname = nil; + + // tvOS only uses a plain launch image. +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) + screenname = [bundle objectForInfoDictionaryKey:@"UILaunchStoryboardName"]; + + if (screenname) { + @try { + /* The launch storyboard is actually a nib in some older versions of + * Xcode. We'll try to load it as a storyboard first, as it's more + * modern. */ + UIStoryboard *storyboard = [UIStoryboard storyboardWithName:screenname bundle:bundle]; + __auto_type storyboardVc = [storyboard instantiateInitialViewController]; + vc = [[SDLLaunchStoryboardViewController alloc] initWithStoryboardViewController:storyboardVc]; + } + @catch (NSException *exception) { + // Do nothing (there's more code to execute below). + } + } +#endif + + if (vc == nil) { + vc = [[SDLLaunchScreenController alloc] initWithNibName:screenname bundle:bundle]; + } + + if (vc.view) { +#ifdef SDL_PLATFORM_VISIONOS + CGRect viewFrame = CGRectMake(0, 0, SDL_XR_SCREENWIDTH, SDL_XR_SCREENHEIGHT); +#else + CGRect viewFrame = [UIScreen mainScreen].bounds; +#endif + launchWindow = [[UIWindow alloc] initWithFrame:viewFrame]; + + /* We don't want the launch window immediately hidden when a real SDL + * window is shown - we fade it out ourselves when we're ready. */ + launchWindow.windowLevel = UIWindowLevelNormal + 1.0; + + /* Show the window but don't make it key. Events should always go to + * other windows when possible. */ + launchWindow.hidden = NO; + + launchWindow.rootViewController = vc; + } +#endif + + // Set working directory to resource path + [[NSFileManager defaultManager] changeCurrentDirectoryPath:[bundle resourcePath]]; + + SDL_SetMainReady(); + [self performSelector:@selector(postFinishLaunch) withObject:nil afterDelay:0.0]; + + return YES; +} + +- (UIWindow *)window +{ + SDL_VideoDevice *_this = SDL_GetVideoDevice(); + if (_this) { + SDL_Window *window = NULL; + for (window = _this->windows; window != NULL; window = window->next) { + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + if (data != nil) { + return data.uiwindow; + } + } + } + return nil; +} + +- (void)setWindow:(UIWindow *)window +{ + // Do nothing. +} + +- (void)sendDropFileForURL:(NSURL *)url fromSourceApplication:(NSString *)sourceApplication +{ + NSURL *fileURL = url.filePathURL; + const char *sourceApplicationCString = sourceApplication ? [sourceApplication UTF8String] : NULL; + if (fileURL != nil) { + SDL_SendDropFile(NULL, sourceApplicationCString, fileURL.path.UTF8String); + } else { + SDL_SendDropFile(NULL, sourceApplicationCString, url.absoluteString.UTF8String); + } + SDL_SendDropComplete(NULL); +} + +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options +{ + // TODO: Handle options + [self sendDropFileForURL:url fromSourceApplication:NULL]; + return YES; +} + +@end + +#endif // SDL_VIDEO_DRIVER_UIKIT diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitclipboard.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitclipboard.h new file mode 100644 index 0000000..1c538c2 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitclipboard.h @@ -0,0 +1,33 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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_uikitclipboard_h_ +#define SDL_uikitclipboard_h_ + +#include "../SDL_sysvideo.h" + +extern bool UIKit_SetClipboardText(SDL_VideoDevice *_this, const char *text); +extern char *UIKit_GetClipboardText(SDL_VideoDevice *_this); +extern bool UIKit_HasClipboardText(SDL_VideoDevice *_this); + +extern void UIKit_InitClipboard(SDL_VideoDevice *_this); +extern void UIKit_QuitClipboard(SDL_VideoDevice *_this); + +#endif // SDL_uikitclipboard_h_ diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitclipboard.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitclipboard.m new file mode 100644 index 0000000..8ed4eb0 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitclipboard.m @@ -0,0 +1,105 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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_uikitvideo.h" +#include "../../events/SDL_clipboardevents_c.h" + +#import + +bool UIKit_SetClipboardText(SDL_VideoDevice *_this, const char *text) +{ +#ifdef SDL_PLATFORM_TVOS + return SDL_SetError("The clipboard is not available on tvOS"); +#else + @autoreleasepool { + [UIPasteboard generalPasteboard].string = @(text); + return true; + } +#endif +} + +char *UIKit_GetClipboardText(SDL_VideoDevice *_this) +{ +#ifdef SDL_PLATFORM_TVOS + return SDL_strdup(""); // Unsupported. +#else + @autoreleasepool { + UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; + NSString *string = pasteboard.string; + + if (string != nil) { + return SDL_strdup(string.UTF8String); + } else { + return SDL_strdup(""); + } + } +#endif +} + +bool UIKit_HasClipboardText(SDL_VideoDevice *_this) +{ + @autoreleasepool { +#ifndef SDL_PLATFORM_TVOS + if ([UIPasteboard generalPasteboard].string != nil) { + return true; + } +#endif + return false; + } +} + +void UIKit_InitClipboard(SDL_VideoDevice *_this) +{ +#ifndef SDL_PLATFORM_TVOS + @autoreleasepool { + SDL_UIKitVideoData *data = (__bridge SDL_UIKitVideoData *)_this->internal; + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + + id observer = [center addObserverForName:UIPasteboardChangedNotification + object:nil + queue:nil + usingBlock:^(NSNotification *note) { + // TODO: compute mime types + SDL_SendClipboardUpdate(false, NULL, 0); + }]; + + data.pasteboardObserver = observer; + } +#endif +} + +void UIKit_QuitClipboard(SDL_VideoDevice *_this) +{ + @autoreleasepool { + SDL_UIKitVideoData *data = (__bridge SDL_UIKitVideoData *)_this->internal; + + if (data.pasteboardObserver != nil) { + [[NSNotificationCenter defaultCenter] removeObserver:data.pasteboardObserver]; + } + + data.pasteboardObserver = nil; + } +} + +#endif // SDL_VIDEO_DRIVER_UIKIT diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitevents.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitevents.h new file mode 100644 index 0000000..c767fb1 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitevents.h @@ -0,0 +1,40 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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_uikitevents_h_ +#define SDL_uikitevents_h_ + +#import + +#include "../SDL_sysvideo.h" + +extern void SDL_UpdateLifecycleObserver(void); + +extern Uint64 UIKit_GetEventTimestamp(NSTimeInterval nsTimestamp); +extern void UIKit_PumpEvents(SDL_VideoDevice *_this); + +extern void SDL_InitGCKeyboard(void); +extern void SDL_QuitGCKeyboard(void); + +extern void SDL_InitGCMouse(void); +extern bool SDL_GCMouseRelativeMode(void); +extern void SDL_QuitGCMouse(void); + +#endif // SDL_uikitevents_h_ diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitevents.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitevents.m new file mode 100644 index 0000000..86224f7 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitevents.m @@ -0,0 +1,461 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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 "../../events/SDL_events_c.h" +#include "../../main/SDL_main_callbacks.h" + +#include "SDL_uikitevents.h" +#include "SDL_uikitopengles.h" +#include "SDL_uikitvideo.h" +#include "SDL_uikitwindow.h" + +#import +#import + +static BOOL UIKit_EventPumpEnabled = YES; + +@interface SDL_LifecycleObserver : NSObject +@property(nonatomic, assign) BOOL isObservingNotifications; +@end + +@implementation SDL_LifecycleObserver + +- (void)update +{ + NSNotificationCenter *notificationCenter = NSNotificationCenter.defaultCenter; + bool wants_observation = (UIKit_EventPumpEnabled || SDL_HasMainCallbacks()); + if (!wants_observation) { + // Make sure no windows have active animation callbacks + int num_windows = 0; + SDL_free(SDL_GetWindows(&num_windows)); + if (num_windows > 0) { + wants_observation = true; + } + } + if (wants_observation && !self.isObservingNotifications) { + self.isObservingNotifications = YES; + [notificationCenter addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil]; + [notificationCenter addObserver:self selector:@selector(applicationWillResignActive) name:UIApplicationWillResignActiveNotification object:nil]; + [notificationCenter addObserver:self selector:@selector(applicationDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil]; + [notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil]; + [notificationCenter addObserver:self selector:@selector(applicationWillTerminate) name:UIApplicationWillTerminateNotification object:nil]; + [notificationCenter addObserver:self selector:@selector(applicationDidReceiveMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) + [notificationCenter addObserver:self + selector:@selector(applicationDidChangeStatusBarOrientation) + name:UIApplicationDidChangeStatusBarOrientationNotification + object:nil]; +#endif + } else if (!wants_observation && self.isObservingNotifications) { + self.isObservingNotifications = NO; + [notificationCenter removeObserver:self]; + } +} + +- (void)applicationDidBecomeActive +{ + SDL_OnApplicationDidEnterForeground(); +} + +- (void)applicationWillResignActive +{ + SDL_OnApplicationWillEnterBackground(); +} + +- (void)applicationDidEnterBackground +{ + SDL_OnApplicationDidEnterBackground(); +} + +- (void)applicationWillEnterForeground +{ + SDL_OnApplicationWillEnterForeground(); +} + +- (void)applicationWillTerminate +{ + SDL_OnApplicationWillTerminate(); +} + +- (void)applicationDidReceiveMemoryWarning +{ + SDL_OnApplicationDidReceiveMemoryWarning(); +} + +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) +- (void)applicationDidChangeStatusBarOrientation +{ + SDL_OnApplicationDidChangeStatusBarOrientation(); +} +#endif + +@end + +void SDL_UpdateLifecycleObserver(void) +{ + static SDL_LifecycleObserver *lifecycleObserver; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + lifecycleObserver = [SDL_LifecycleObserver new]; + }); + [lifecycleObserver update]; +} + +void SDL_SetiOSEventPump(bool enabled) +{ + UIKit_EventPumpEnabled = enabled; + + SDL_UpdateLifecycleObserver(); +} + +Uint64 UIKit_GetEventTimestamp(NSTimeInterval nsTimestamp) +{ + static Uint64 timestamp_offset; + Uint64 timestamp = (Uint64)(nsTimestamp * SDL_NS_PER_SECOND); + Uint64 now = SDL_GetTicksNS(); + + if (!timestamp_offset) { + timestamp_offset = (now - timestamp); + } + timestamp += timestamp_offset; + + if (timestamp > now) { + timestamp_offset -= (timestamp - now); + timestamp = now; + } + return timestamp; +} + +void UIKit_PumpEvents(SDL_VideoDevice *_this) +{ + if (!UIKit_EventPumpEnabled) { + return; + } + + /* Let the run loop run for a short amount of time: long enough for + touch events to get processed (which is important to get certain + elements of Game Center's GKLeaderboardViewController to respond + to touch input), but not long enough to introduce a significant + delay in the rest of the app. + */ + const CFTimeInterval seconds = 0.000002; + + // Pump most event types. + SInt32 result; + do { + result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, seconds, TRUE); + } while (result == kCFRunLoopRunHandledSource); + + // Make sure UIScrollView objects scroll properly. + do { + result = CFRunLoopRunInMode((CFStringRef)UITrackingRunLoopMode, seconds, TRUE); + } while (result == kCFRunLoopRunHandledSource); + + // See the comment in the function definition. +#if defined(SDL_VIDEO_OPENGL_ES) || defined(SDL_VIDEO_OPENGL_ES2) + UIKit_GL_RestoreCurrentContext(); +#endif +} + +static id keyboard_connect_observer = nil; +static id keyboard_disconnect_observer = nil; + +static void OnGCKeyboardConnected(GCKeyboard *keyboard) API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) +{ + SDL_KeyboardID keyboardID = (SDL_KeyboardID)(uintptr_t)keyboard; + + SDL_AddKeyboard(keyboardID, NULL, true); + + keyboard.keyboardInput.keyChangedHandler = ^(GCKeyboardInput *kbrd, GCControllerButtonInput *key, GCKeyCode keyCode, BOOL pressed) { + Uint64 timestamp = SDL_GetTicksNS(); + SDL_SendKeyboardKey(timestamp, keyboardID, 0, (SDL_Scancode)keyCode, pressed); + }; + + dispatch_queue_t queue = dispatch_queue_create("org.libsdl.input.keyboard", DISPATCH_QUEUE_SERIAL); + dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); + keyboard.handlerQueue = queue; +} + +static void OnGCKeyboardDisconnected(GCKeyboard *keyboard) API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) +{ + SDL_KeyboardID keyboardID = (SDL_KeyboardID)(uintptr_t)keyboard; + + SDL_RemoveKeyboard(keyboardID, true); + + keyboard.keyboardInput.keyChangedHandler = nil; +} + +void SDL_InitGCKeyboard(void) +{ + @autoreleasepool { + if (@available(iOS 14.0, tvOS 14.0, *)) { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + + keyboard_connect_observer = [center addObserverForName:GCKeyboardDidConnectNotification + object:nil + queue:nil + usingBlock:^(NSNotification *note) { + GCKeyboard *keyboard = note.object; + OnGCKeyboardConnected(keyboard); + }]; + + keyboard_disconnect_observer = [center addObserverForName:GCKeyboardDidDisconnectNotification + object:nil + queue:nil + usingBlock:^(NSNotification *note) { + GCKeyboard *keyboard = note.object; + OnGCKeyboardDisconnected(keyboard); + }]; + + if (GCKeyboard.coalescedKeyboard != nil) { + OnGCKeyboardConnected(GCKeyboard.coalescedKeyboard); + } + } + } +} + +void SDL_QuitGCKeyboard(void) +{ + @autoreleasepool { + if (@available(iOS 14.0, tvOS 14.0, *)) { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + + if (keyboard_connect_observer) { + [center removeObserver:keyboard_connect_observer name:GCKeyboardDidConnectNotification object:nil]; + keyboard_connect_observer = nil; + } + + if (keyboard_disconnect_observer) { + [center removeObserver:keyboard_disconnect_observer name:GCKeyboardDidDisconnectNotification object:nil]; + keyboard_disconnect_observer = nil; + } + + if (GCKeyboard.coalescedKeyboard != nil) { + OnGCKeyboardDisconnected(GCKeyboard.coalescedKeyboard); + } + } + } +} + +static id mouse_connect_observer = nil; +static id mouse_disconnect_observer = nil; +static bool mouse_relative_mode = false; +static SDL_MouseWheelDirection mouse_scroll_direction = SDL_MOUSEWHEEL_NORMAL; + +static void UpdateScrollDirection(void) +{ +#if 0 // This code doesn't work for some reason + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + if ([userDefaults boolForKey:@"com.apple.swipescrolldirection"]) { + mouse_scroll_direction = SDL_MOUSEWHEEL_FLIPPED; + } else { + mouse_scroll_direction = SDL_MOUSEWHEEL_NORMAL; + } +#else + Boolean keyExistsAndHasValidFormat = NO; + Boolean naturalScrollDirection = CFPreferencesGetAppBooleanValue(CFSTR("com.apple.swipescrolldirection"), kCFPreferencesAnyApplication, &keyExistsAndHasValidFormat); + if (!keyExistsAndHasValidFormat) { + // Couldn't read the preference, assume natural scrolling direction + naturalScrollDirection = YES; + } + if (naturalScrollDirection) { + mouse_scroll_direction = SDL_MOUSEWHEEL_FLIPPED; + } else { + mouse_scroll_direction = SDL_MOUSEWHEEL_NORMAL; + } +#endif +} + +static void UpdatePointerLock(void) +{ + SDL_VideoDevice *_this = SDL_GetVideoDevice(); + SDL_Window *window; + + for (window = _this->windows; window != NULL; window = window->next) { + UIKit_UpdatePointerLock(_this, window); + } +} + +static bool SetGCMouseRelativeMode(bool enabled) +{ + mouse_relative_mode = enabled; + UpdatePointerLock(); + return true; +} + +static void OnGCMouseButtonChanged(SDL_MouseID mouseID, Uint8 button, BOOL pressed) +{ + Uint64 timestamp = SDL_GetTicksNS(); + SDL_SendMouseButton(timestamp, SDL_GetMouseFocus(), mouseID, button, pressed); +} + +static void OnGCMouseConnected(GCMouse *mouse) API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) +{ + SDL_MouseID mouseID = (SDL_MouseID)(uintptr_t)mouse; + + SDL_AddMouse(mouseID, NULL, true); + + mouse.mouseInput.leftButton.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) { + OnGCMouseButtonChanged(mouseID, SDL_BUTTON_LEFT, pressed); + }; + mouse.mouseInput.middleButton.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) { + OnGCMouseButtonChanged(mouseID, SDL_BUTTON_MIDDLE, pressed); + }; + mouse.mouseInput.rightButton.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) { + OnGCMouseButtonChanged(mouseID, SDL_BUTTON_RIGHT, pressed); + }; + + int auxiliary_button = SDL_BUTTON_X1; + for (GCControllerButtonInput *btn in mouse.mouseInput.auxiliaryButtons) { + btn.pressedChangedHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) { + OnGCMouseButtonChanged(mouseID, auxiliary_button, pressed); + }; + ++auxiliary_button; + } + + mouse.mouseInput.mouseMovedHandler = ^(GCMouseInput *mouseInput, float deltaX, float deltaY) { + Uint64 timestamp = SDL_GetTicksNS(); + + if (SDL_GCMouseRelativeMode()) { + SDL_SendMouseMotion(timestamp, SDL_GetMouseFocus(), mouseID, true, deltaX, -deltaY); + } + }; + + mouse.mouseInput.scroll.valueChangedHandler = ^(GCControllerDirectionPad *dpad, float xValue, float yValue) { + Uint64 timestamp = SDL_GetTicksNS(); + + /* Raw scroll values come in here, vertical values in the first axis, horizontal values in the second axis. + * The vertical values are negative moving the mouse wheel up and positive moving it down. + * The horizontal values are negative moving the mouse wheel left and positive moving it right. + * The vertical values are inverted compared to SDL, and the horizontal values are as expected. + */ + float vertical = -xValue; + float horizontal = yValue; + + if (mouse_scroll_direction == SDL_MOUSEWHEEL_FLIPPED) { + // Since these are raw values, we need to flip them ourselves + vertical = -vertical; + horizontal = -horizontal; + } + SDL_SendMouseWheel(timestamp, SDL_GetMouseFocus(), mouseID, horizontal, vertical, mouse_scroll_direction); + }; + UpdateScrollDirection(); + + dispatch_queue_t queue = dispatch_queue_create("org.libsdl.input.mouse", DISPATCH_QUEUE_SERIAL); + dispatch_set_target_queue(queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); + mouse.handlerQueue = queue; + + UpdatePointerLock(); +} + +static void OnGCMouseDisconnected(GCMouse *mouse) API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) +{ + SDL_MouseID mouseID = (SDL_MouseID)(uintptr_t)mouse; + + mouse.mouseInput.mouseMovedHandler = nil; + + mouse.mouseInput.leftButton.pressedChangedHandler = nil; + mouse.mouseInput.middleButton.pressedChangedHandler = nil; + mouse.mouseInput.rightButton.pressedChangedHandler = nil; + + for (GCControllerButtonInput *button in mouse.mouseInput.auxiliaryButtons) { + button.pressedChangedHandler = nil; + } + + UpdatePointerLock(); + + SDL_RemoveMouse(mouseID, true); +} + +void SDL_InitGCMouse(void) +{ + @autoreleasepool { + // There is a bug where mouse accumulates duplicate deltas over time in iOS 14.0 + if (@available(iOS 14.1, tvOS 14.1, *)) { + /* iOS will not send the new pointer touch events if you don't have this key, + * and we need them to differentiate between mouse events and real touch events. + */ + BOOL indirect_input_available = [[[[NSBundle mainBundle] infoDictionary] objectForKey:@"UIApplicationSupportsIndirectInputEvents"] boolValue]; + if (indirect_input_available) { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + + mouse_connect_observer = [center addObserverForName:GCMouseDidConnectNotification + object:nil + queue:nil + usingBlock:^(NSNotification *note) { + GCMouse *mouse = note.object; + OnGCMouseConnected(mouse); + }]; + + mouse_disconnect_observer = [center addObserverForName:GCMouseDidDisconnectNotification + object:nil + queue:nil + usingBlock:^(NSNotification *note) { + GCMouse *mouse = note.object; + OnGCMouseDisconnected(mouse); + }]; + + for (GCMouse *mouse in [GCMouse mice]) { + OnGCMouseConnected(mouse); + } + + SDL_GetMouse()->SetRelativeMouseMode = SetGCMouseRelativeMode; + } else { + NSLog(@"You need UIApplicationSupportsIndirectInputEvents in your Info.plist for mouse support"); + } + } + } +} + +bool SDL_GCMouseRelativeMode(void) +{ + return mouse_relative_mode; +} + +void SDL_QuitGCMouse(void) +{ + @autoreleasepool { + if (@available(iOS 14.1, tvOS 14.1, *)) { + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + + if (mouse_connect_observer) { + [center removeObserver:mouse_connect_observer name:GCMouseDidConnectNotification object:nil]; + mouse_connect_observer = nil; + } + + if (mouse_disconnect_observer) { + [center removeObserver:mouse_disconnect_observer name:GCMouseDidDisconnectNotification object:nil]; + mouse_disconnect_observer = nil; + } + + for (GCMouse *mouse in [GCMouse mice]) { + OnGCMouseDisconnected(mouse); + } + + SDL_GetMouse()->SetRelativeMouseMode = NULL; + } + } +} + +#endif // SDL_VIDEO_DRIVER_UIKIT diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmessagebox.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmessagebox.h new file mode 100644 index 0000000..5c21f7c --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmessagebox.h @@ -0,0 +1,28 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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 + +extern bool UIKit_ShowingMessageBox(void); +extern bool UIKit_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonID); + +#endif // SDL_VIDEO_DRIVER_UIKIT diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmessagebox.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmessagebox.m new file mode 100644 index 0000000..a57b3f7 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmessagebox.m @@ -0,0 +1,154 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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_uikitvideo.h" +#include "SDL_uikitwindow.h" + +// Display a UIKit message box + +static bool s_showingMessageBox = false; + +bool UIKit_ShowingMessageBox(void) +{ + return s_showingMessageBox; +} + +static void UIKit_WaitUntilMessageBoxClosed(const SDL_MessageBoxData *messageboxdata, int *clickedindex) +{ + *clickedindex = messageboxdata->numbuttons; + + @autoreleasepool { + // Run the main event loop until the alert has finished + // Note that this needs to be done on the main thread + s_showingMessageBox = true; + while ((*clickedindex) == messageboxdata->numbuttons) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + s_showingMessageBox = false; + } +} + +static BOOL UIKit_ShowMessageBoxAlertController(const SDL_MessageBoxData *messageboxdata, int *buttonID) +{ + int i; + int __block clickedindex = messageboxdata->numbuttons; + UIWindow *window = nil; + UIWindow *alertwindow = nil; + + if (![UIAlertController class]) { + return NO; + } + + UIAlertController *alert; + alert = [UIAlertController alertControllerWithTitle:@(messageboxdata->title) + message:@(messageboxdata->message) + preferredStyle:UIAlertControllerStyleAlert]; + + for (i = 0; i < messageboxdata->numbuttons; i++) { + UIAlertAction *action; + UIAlertActionStyle style = UIAlertActionStyleDefault; + const SDL_MessageBoxButtonData *sdlButton; + + if (messageboxdata->flags & SDL_MESSAGEBOX_BUTTONS_RIGHT_TO_LEFT) { + sdlButton = &messageboxdata->buttons[messageboxdata->numbuttons - 1 - i]; + } else { + sdlButton = &messageboxdata->buttons[i]; + } + + if (sdlButton->flags & SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT) { + style = UIAlertActionStyleCancel; + } + + action = [UIAlertAction actionWithTitle:@(sdlButton->text) + style:style + handler:^(UIAlertAction *alertAction) { + clickedindex = (int)(sdlButton - messageboxdata->buttons); + }]; + [alert addAction:action]; + + if (sdlButton->flags & SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT) { + alert.preferredAction = action; + } + } + + if (messageboxdata->window) { + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)messageboxdata->window->internal; + window = data.uiwindow; + } + + if (window == nil || window.rootViewController == nil) { +#ifdef SDL_PLATFORM_VISIONOS + alertwindow = [[UIWindow alloc] init]; +#else + alertwindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; +#endif + alertwindow.rootViewController = [UIViewController new]; + alertwindow.windowLevel = UIWindowLevelAlert; + + window = alertwindow; + + [alertwindow makeKeyAndVisible]; + } + + [window.rootViewController presentViewController:alert animated:YES completion:nil]; + UIKit_WaitUntilMessageBoxClosed(messageboxdata, &clickedindex); + + if (alertwindow) { + alertwindow.hidden = YES; + } + + UIKit_ForceUpdateHomeIndicator(); + + *buttonID = messageboxdata->buttons[clickedindex].buttonID; + return YES; +} + +static void UIKit_ShowMessageBoxImpl(const SDL_MessageBoxData *messageboxdata, int *buttonID, int *result) +{ + @autoreleasepool { + if (UIKit_ShowMessageBoxAlertController(messageboxdata, buttonID)) { + *result = true; + } else { + *result = SDL_SetError("Could not show message box."); + } + } +} + +bool UIKit_ShowMessageBox(const SDL_MessageBoxData *messageboxdata, int *buttonID) +{ + @autoreleasepool { + __block int result = true; + + if ([NSThread isMainThread]) { + UIKit_ShowMessageBoxImpl(messageboxdata, buttonID, &result); + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + UIKit_ShowMessageBoxImpl(messageboxdata, buttonID, &result); + }); + } + return result; + } +} + +#endif // SDL_VIDEO_DRIVER_UIKIT diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmetalview.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmetalview.h new file mode 100644 index 0000000..20bcf7c --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmetalview.h @@ -0,0 +1,54 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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. + */ + +/* + * @author Mark Callow, www.edgewise-consulting.com. + * + * Thanks to @slime73 on GitHub for their gist showing how to add a CAMetalLayer + * backed view. + */ + +#ifndef SDL_uikitmetalview_h_ +#define SDL_uikitmetalview_h_ + +#include "../SDL_sysvideo.h" +#include "SDL_uikitwindow.h" + +#if defined(SDL_VIDEO_DRIVER_UIKIT) && (defined(SDL_VIDEO_VULKAN) || defined(SDL_VIDEO_METAL)) + +#import +#import +#import + +@interface SDL_uikitmetalview : SDL_uikitview + +- (instancetype)initWithFrame:(CGRect)frame + scale:(CGFloat)scale; + +@end + +SDL_MetalView UIKit_Metal_CreateView(SDL_VideoDevice *_this, SDL_Window *window); +void UIKit_Metal_DestroyView(SDL_VideoDevice *_this, SDL_MetalView view); +void *UIKit_Metal_GetLayer(SDL_VideoDevice *_this, SDL_MetalView view); + +#endif // SDL_VIDEO_DRIVER_UIKIT && (SDL_VIDEO_VULKAN || SDL_VIDEO_METAL) + +#endif // SDL_uikitmetalview_h_ diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmetalview.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmetalview.m new file mode 100644 index 0000000..010dae4 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmetalview.m @@ -0,0 +1,140 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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. + */ + +/* + * @author Mark Callow, www.edgewise-consulting.com. + * + * Thanks to @slime73 on GitHub for their gist showing how to add a CAMetalLayer + * backed view. + */ + +#include "SDL_internal.h" + +#if defined(SDL_VIDEO_DRIVER_UIKIT) && (defined(SDL_VIDEO_VULKAN) || defined(SDL_VIDEO_METAL)) + +#include "../SDL_sysvideo.h" +#include "../../events/SDL_windowevents_c.h" + +#import "SDL_uikitwindow.h" +#import "SDL_uikitmetalview.h" + +@implementation SDL_uikitmetalview + +// Returns a Metal-compatible layer. ++ (Class)layerClass +{ + return [CAMetalLayer class]; +} + +- (instancetype)initWithFrame:(CGRect)frame + scale:(CGFloat)scale +{ + if ((self = [super initWithFrame:frame])) { + self.tag = SDL_METALVIEW_TAG; + self.layer.contentsScale = scale; + [self updateDrawableSize]; + } + + return self; +} + +// Set the size of the metal drawables when the view is resized. +- (void)layoutSubviews +{ + [super layoutSubviews]; + [self updateDrawableSize]; +} + +- (void)updateDrawableSize +{ + CGSize size = self.bounds.size; + size.width *= self.layer.contentsScale; + size.height *= self.layer.contentsScale; + + CAMetalLayer *metallayer = ((CAMetalLayer *)self.layer); + if (metallayer.drawableSize.width != size.width || + metallayer.drawableSize.height != size.height) { + metallayer.drawableSize = size; + SDL_SendWindowEvent([self getSDLWindow], SDL_EVENT_WINDOW_METAL_VIEW_RESIZED, 0, 0); + } +} + +@end + +SDL_MetalView UIKit_Metal_CreateView(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + CGFloat scale = 1.0; + SDL_uikitmetalview *metalview; + + if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) { + /* Set the scale to the natural scale factor of the screen - then + * the backing dimensions of the Metal view will match the pixel + * dimensions of the screen rather than the dimensions in points + * yielding high resolution on retine displays. + */ +#ifndef SDL_PLATFORM_VISIONOS + scale = data.uiwindow.screen.nativeScale; +#else + // VisionOS doesn't use the concept of "nativeScale" like other iOS devices. + // We use a fixed scale factor of 2.0 to achieve better pixel density. + // This is because VisionOS presents a virtual 1280x720 "screen", but we need + // to render at a higher resolution for optimal visual quality. + // TODO: Consider making this configurable or determining it dynamically + // based on the specific visionOS device capabilities. + scale = 2.0; +#endif + } + + metalview = [[SDL_uikitmetalview alloc] initWithFrame:data.uiwindow.bounds + scale:scale]; + if (metalview == nil) { + SDL_OutOfMemory(); + return NULL; + } + + [metalview setSDLWindow:window]; + + return (void *)CFBridgingRetain(metalview); + } +} + +void UIKit_Metal_DestroyView(SDL_VideoDevice *_this, SDL_MetalView view) +{ + @autoreleasepool { + SDL_uikitmetalview *metalview = CFBridgingRelease(view); + + if ([metalview isKindOfClass:[SDL_uikitmetalview class]]) { + [metalview setSDLWindow:NULL]; + } + } +} + +void *UIKit_Metal_GetLayer(SDL_VideoDevice *_this, SDL_MetalView view) +{ + @autoreleasepool { + SDL_uikitview *uiview = (__bridge SDL_uikitview *)view; + return (__bridge void *)uiview.layer; + } +} + +#endif // SDL_VIDEO_DRIVER_UIKIT && (SDL_VIDEO_VULKAN || SDL_VIDEO_METAL) diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmodes.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmodes.h new file mode 100644 index 0000000..1b25cfd --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmodes.h @@ -0,0 +1,67 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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" + +#ifndef SDL_uikitmodes_h_ +#define SDL_uikitmodes_h_ + +#include "SDL_uikitvideo.h" + +@interface SDL_UIKitDisplayData : NSObject + +#ifndef SDL_PLATFORM_VISIONOS +- (instancetype)initWithScreen:(UIScreen *)screen; +@property(nonatomic, strong) UIScreen *uiscreen; +#endif + +@end + +@interface SDL_UIKitDisplayModeData : NSObject +#ifndef SDL_PLATFORM_VISIONOS +@property(nonatomic, strong) UIScreenMode *uiscreenmode; +#endif + +@end + +#ifndef SDL_PLATFORM_VISIONOS +extern bool UIKit_IsDisplayLandscape(UIScreen *uiscreen); +#endif + +extern bool UIKit_InitModes(SDL_VideoDevice *_this); +#ifndef SDL_PLATFORM_VISIONOS +extern bool UIKit_AddDisplay(UIScreen *uiscreen, bool send_event); +extern void UIKit_DelDisplay(UIScreen *uiscreen, bool send_event); +#endif +extern bool UIKit_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display); +extern bool UIKit_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_DisplayMode *mode); +extern void UIKit_QuitModes(SDL_VideoDevice *_this); +extern bool UIKit_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect); + +// because visionOS does not have a screen +// we create a fake display to maintain compatibility. +// By default, a window measures 1280x720 pt. +// https://developer.apple.com/design/human-interface-guidelines/windows#visionOS +#ifdef SDL_PLATFORM_VISIONOS +#define SDL_XR_SCREENWIDTH 1280 +#define SDL_XR_SCREENHEIGHT 720 +#endif + +#endif // SDL_uikitmodes_h_ diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmodes.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmodes.m new file mode 100644 index 0000000..d3247db --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitmodes.m @@ -0,0 +1,543 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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_uikitmodes.h" + +#include "../../events/SDL_events_c.h" + +#import + +@implementation SDL_UIKitDisplayData + +#ifndef SDL_PLATFORM_VISIONOS +- (instancetype)initWithScreen:(UIScreen *)screen +{ + if (self = [super init]) { + self.uiscreen = screen; + } + return self; +} +@synthesize uiscreen; +#endif + +@end + +@implementation SDL_UIKitDisplayModeData + +#ifndef SDL_PLATFORM_VISIONOS +@synthesize uiscreenmode; +#endif + +@end + +@interface SDL_DisplayWatch : NSObject +@end + +#ifndef SDL_PLATFORM_VISIONOS +@implementation SDL_DisplayWatch + ++ (void)start +{ + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + + [center addObserver:self + selector:@selector(screenConnected:) + name:UIScreenDidConnectNotification + object:nil]; + [center addObserver:self + selector:@selector(screenDisconnected:) + name:UIScreenDidDisconnectNotification + object:nil]; +} + ++ (void)stop +{ + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + + [center removeObserver:self + name:UIScreenDidConnectNotification + object:nil]; + [center removeObserver:self + name:UIScreenDidDisconnectNotification + object:nil]; +} + ++ (void)screenConnected:(NSNotification *)notification +{ + UIScreen *uiscreen = [notification object]; + UIKit_AddDisplay(uiscreen, true); +} + ++ (void)screenDisconnected:(NSNotification *)notification +{ + UIScreen *uiscreen = [notification object]; + UIKit_DelDisplay(uiscreen, true); +} + +@end +#endif + +#ifndef SDL_PLATFORM_VISIONOS +static bool UIKit_AllocateDisplayModeData(SDL_DisplayMode *mode, + UIScreenMode *uiscreenmode) +{ + SDL_UIKitDisplayModeData *data = nil; + + if (uiscreenmode != nil) { + // Allocate the display mode data + data = [[SDL_UIKitDisplayModeData alloc] init]; + if (!data) { + return SDL_OutOfMemory(); + } + + data.uiscreenmode = uiscreenmode; + } + + mode->internal = (void *)CFBridgingRetain(data); + + return true; +} +#endif + +static void UIKit_FreeDisplayModeData(SDL_DisplayMode *mode) +{ + if (mode->internal != NULL) { + CFRelease(mode->internal); + mode->internal = NULL; + } +} + +#ifndef SDL_PLATFORM_VISIONOS +static float UIKit_GetDisplayModeRefreshRate(UIScreen *uiscreen) +{ + return (float)uiscreen.maximumFramesPerSecond; +} + +static bool UIKit_AddSingleDisplayMode(SDL_VideoDisplay *display, int w, int h, + UIScreen *uiscreen, UIScreenMode *uiscreenmode) +{ + SDL_DisplayMode mode; + + SDL_zero(mode); + if (!UIKit_AllocateDisplayModeData(&mode, uiscreenmode)) { + return false; + } + + mode.w = w; + mode.h = h; + mode.pixel_density = uiscreen.nativeScale; + mode.refresh_rate = UIKit_GetDisplayModeRefreshRate(uiscreen); + mode.format = SDL_PIXELFORMAT_ABGR8888; + + if (SDL_AddFullscreenDisplayMode(display, &mode)) { + return true; + } else { + UIKit_FreeDisplayModeData(&mode); + return false; + } +} + +static bool UIKit_AddDisplayMode(SDL_VideoDisplay *display, int w, int h, + UIScreen *uiscreen, UIScreenMode *uiscreenmode, bool addRotation) +{ + if (!UIKit_AddSingleDisplayMode(display, w, h, uiscreen, uiscreenmode)) { + return false; + } + + if (addRotation) { + // Add the rotated version + if (!UIKit_AddSingleDisplayMode(display, h, w, uiscreen, uiscreenmode)) { + return false; + } + } + + return true; +} + +static CGSize GetUIScreenModeSize(UIScreen *uiscreen, UIScreenMode *mode) +{ + /* For devices such as iPhone 6/7/8 Plus, the UIScreenMode reported by iOS + * isn't the physical pixels of the display, but rather the point size times + * the scale. For example, on iOS 12.2 on iPhone 8 Plus the physical pixel + * resolution is 1080x1920, the size reported by mode.size is 1242x2208, + * the size in points is 414x736, the scale property is 3.0, and the + * nativeScale property is ~2.6087 (ie 1920.0 / 736.0). + * + * What we want for the mode size is the point size, and the pixel density + * is the native scale. + * + * Note that the iOS Simulator doesn't have this behavior for those devices. + * https://github.com/libsdl-org/SDL/issues/3220 + */ + CGSize size = mode.size; + + size.width = SDL_round(size.width / uiscreen.scale); + size.height = SDL_round(size.height / uiscreen.scale); + + return size; +} + +bool UIKit_AddDisplay(UIScreen *uiscreen, bool send_event) +{ + UIScreenMode *uiscreenmode = uiscreen.currentMode; + CGSize size = GetUIScreenModeSize(uiscreen, uiscreenmode); + SDL_VideoDisplay display; + SDL_DisplayMode mode; + + // Make sure the width/height are oriented correctly + if (UIKit_IsDisplayLandscape(uiscreen) != (size.width > size.height)) { + CGFloat height = size.width; + size.width = size.height; + size.height = height; + } + + SDL_zero(mode); + mode.w = (int)size.width; + mode.h = (int)size.height; + mode.pixel_density = uiscreen.nativeScale; + mode.format = SDL_PIXELFORMAT_ABGR8888; + mode.refresh_rate = UIKit_GetDisplayModeRefreshRate(uiscreen); + + if (!UIKit_AllocateDisplayModeData(&mode, uiscreenmode)) { + return false; + } + + SDL_zero(display); +#ifndef SDL_PLATFORM_TVOS + if (uiscreen == [UIScreen mainScreen]) { + // The natural orientation (used by sensors) is portrait + display.natural_orientation = SDL_ORIENTATION_PORTRAIT; + } else +#endif + if (UIKit_IsDisplayLandscape(uiscreen)) { + display.natural_orientation = SDL_ORIENTATION_LANDSCAPE; + } else { + display.natural_orientation = SDL_ORIENTATION_PORTRAIT; + } + display.desktop_mode = mode; + + display.HDR.SDR_white_level = 1.0f; + display.HDR.HDR_headroom = 1.0f; + +#ifndef SDL_PLATFORM_TVOS + if (@available(iOS 16.0, *)) { + if (uiscreen.currentEDRHeadroom > 1.0f) { + display.HDR.HDR_headroom = uiscreen.currentEDRHeadroom; + } else { + display.HDR.HDR_headroom = uiscreen.potentialEDRHeadroom; + } + } +#endif // !SDL_PLATFORM_TVOS + + // Allocate the display data +#ifdef SDL_PLATFORM_VISIONOS + SDL_UIKitDisplayData *data = [[SDL_UIKitDisplayData alloc] init]; +#else + SDL_UIKitDisplayData *data = [[SDL_UIKitDisplayData alloc] initWithScreen:uiscreen]; +#endif + if (!data) { + UIKit_FreeDisplayModeData(&display.desktop_mode); + return SDL_OutOfMemory(); + } + + display.internal = (SDL_DisplayData *)CFBridgingRetain(data); + if (SDL_AddVideoDisplay(&display, send_event) == 0) { + return false; + } + return true; +} +#endif + +#ifdef SDL_PLATFORM_VISIONOS +bool UIKit_AddDisplay(bool send_event){ + CGSize size = CGSizeMake(SDL_XR_SCREENWIDTH, SDL_XR_SCREENHEIGHT); + SDL_VideoDisplay display; + SDL_DisplayMode mode; + + SDL_zero(mode); + mode.w = (int)size.width; + mode.h = (int)size.height; + mode.pixel_density = 1; + mode.format = SDL_PIXELFORMAT_ABGR8888; + mode.refresh_rate = 60.0f; + + display.natural_orientation = SDL_ORIENTATION_LANDSCAPE; + + display.desktop_mode = mode; + + SDL_UIKitDisplayData *data = [[SDL_UIKitDisplayData alloc] init]; + + if (!data) { + UIKit_FreeDisplayModeData(&display.desktop_mode); + return SDL_OutOfMemory(); + } + + display.internal = (SDL_DisplayData *)CFBridgingRetain(data); + if (SDL_AddVideoDisplay(&display, send_event) == 0) { + return false; + } + return true; +} +#endif + +#ifndef SDL_PLATFORM_VISIONOS + +void UIKit_DelDisplay(UIScreen *uiscreen, bool send_event) +{ + SDL_DisplayID *displays; + int i; + + displays = SDL_GetDisplays(NULL); + if (displays) { + for (i = 0; displays[i]; ++i) { + SDL_VideoDisplay *display = SDL_GetVideoDisplay(displays[i]); + SDL_UIKitDisplayData *data = (__bridge SDL_UIKitDisplayData *)display->internal; + + if (data && data.uiscreen == uiscreen) { + CFRelease(display->internal); + display->internal = NULL; + SDL_DelVideoDisplay(displays[i], send_event); + break; + } + } + SDL_free(displays); + } +} + +bool UIKit_IsDisplayLandscape(UIScreen *uiscreen) +{ +#ifndef SDL_PLATFORM_TVOS + if (uiscreen == [UIScreen mainScreen]) { + return UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation); + } else +#endif // !SDL_PLATFORM_TVOS + { + CGSize size = uiscreen.bounds.size; + return (size.width > size.height); + } +} +#endif +bool UIKit_InitModes(SDL_VideoDevice *_this) +{ + @autoreleasepool { +#ifdef SDL_PLATFORM_VISIONOS + UIKit_AddDisplay(false); +#else + for (UIScreen *uiscreen in [UIScreen screens]) { + if (!UIKit_AddDisplay(uiscreen, false)) { + return false; + } + } +#endif + +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) + SDL_OnApplicationDidChangeStatusBarOrientation(); +#endif + +#ifndef SDL_PLATFORM_VISIONOS + [SDL_DisplayWatch start]; +#endif + } + + return true; +} + +bool UIKit_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display) +{ +#ifndef SDL_PLATFORM_VISIONOS + @autoreleasepool { + SDL_UIKitDisplayData *data = (__bridge SDL_UIKitDisplayData *)display->internal; + + bool isLandscape = UIKit_IsDisplayLandscape(data.uiscreen); + bool addRotation = (data.uiscreen == [UIScreen mainScreen]); + NSArray *availableModes = nil; + +#ifdef SDL_PLATFORM_TVOS + addRotation = false; + availableModes = @[ data.uiscreen.currentMode ]; +#else + availableModes = data.uiscreen.availableModes; +#endif + + for (UIScreenMode *uimode in availableModes) { + CGSize size = GetUIScreenModeSize(data.uiscreen, uimode); + int w = (int)size.width; + int h = (int)size.height; + + // Make sure the width/height are oriented correctly + if (isLandscape != (w > h)) { + int tmp = w; + w = h; + h = tmp; + } + + UIKit_AddDisplayMode(display, w, h, data.uiscreen, uimode, addRotation); + } + } +#endif + return true; +} + +bool UIKit_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_DisplayMode *mode) +{ +#ifndef SDL_PLATFORM_VISIONOS + @autoreleasepool { + SDL_UIKitDisplayData *data = (__bridge SDL_UIKitDisplayData *)display->internal; + +#ifndef SDL_PLATFORM_TVOS + SDL_UIKitDisplayModeData *modedata = (__bridge SDL_UIKitDisplayModeData *)mode->internal; + [data.uiscreen setCurrentMode:modedata.uiscreenmode]; +#endif + + if (data.uiscreen == [UIScreen mainScreen]) { + /* [UIApplication setStatusBarOrientation:] no longer works reliably + * in recent iOS versions, so we can't rotate the screen when setting + * the display mode. */ + if (mode->w > mode->h) { + if (!UIKit_IsDisplayLandscape(data.uiscreen)) { + return SDL_SetError("Screen orientation does not match display mode size"); + } + } else if (mode->w < mode->h) { + if (UIKit_IsDisplayLandscape(data.uiscreen)) { + return SDL_SetError("Screen orientation does not match display mode size"); + } + } + } + } +#endif + return true; +} + +bool UIKit_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_Rect *rect) +{ + @autoreleasepool { + SDL_UIKitDisplayData *data = (__bridge SDL_UIKitDisplayData *)display->internal; +#ifdef SDL_PLATFORM_VISIONOS + CGRect frame = CGRectMake(0, 0, SDL_XR_SCREENWIDTH, SDL_XR_SCREENHEIGHT); +#else + CGRect frame = data.uiscreen.bounds; +#endif + + /* the default function iterates displays to make a fake offset, + as if all the displays were side-by-side, which is fine for iOS. */ + if (!SDL_GetDisplayBounds(display->id, rect)) { + return false; + } + + rect->x += (int)frame.origin.x; + rect->y += (int)frame.origin.y; + rect->w = (int)frame.size.width; + rect->h = (int)frame.size.height; + } + + return true; +} + +void UIKit_QuitModes(SDL_VideoDevice *_this) +{ +#ifndef SDL_PLATFORM_VISIONOS + [SDL_DisplayWatch stop]; +#endif + + // Release Objective-C objects, so higher level doesn't free() them. + int i, j; + @autoreleasepool { + for (i = 0; i < _this->num_displays; i++) { + SDL_VideoDisplay *display = _this->displays[i]; + + UIKit_FreeDisplayModeData(&display->desktop_mode); + for (j = 0; j < display->num_fullscreen_modes; j++) { + SDL_DisplayMode *mode = &display->fullscreen_modes[j]; + UIKit_FreeDisplayModeData(mode); + } + + if (display->internal != NULL) { + CFRelease(display->internal); + display->internal = NULL; + } + } + } +} + +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) +void SDL_OnApplicationDidChangeStatusBarOrientation(void) +{ + BOOL isLandscape = UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation); + SDL_VideoDisplay *display = SDL_GetVideoDisplay(SDL_GetPrimaryDisplay()); + + if (display) { + SDL_DisplayMode *mode = &display->desktop_mode; + SDL_DisplayOrientation orientation = SDL_ORIENTATION_UNKNOWN; + int i; + + /* The desktop display mode should be kept in sync with the screen + * orientation so that updating a window's fullscreen state to + * fullscreen desktop keeps the window dimensions in the + * correct orientation. */ + if (isLandscape != (mode->w > mode->h)) { + SDL_DisplayMode new_mode; + SDL_copyp(&new_mode, mode); + new_mode.w = mode->h; + new_mode.h = mode->w; + + // Make sure we don't free the current display mode data + mode->internal = NULL; + + SDL_SetDesktopDisplayMode(display, &new_mode); + } + + // Same deal with the fullscreen modes + for (i = 0; i < display->num_fullscreen_modes; ++i) { + mode = &display->fullscreen_modes[i]; + if (isLandscape != (mode->w > mode->h)) { + int height = mode->w; + mode->w = mode->h; + mode->h = height; + } + } + + switch ([UIApplication sharedApplication].statusBarOrientation) { + case UIInterfaceOrientationPortrait: + orientation = SDL_ORIENTATION_PORTRAIT; + break; + case UIInterfaceOrientationPortraitUpsideDown: + orientation = SDL_ORIENTATION_PORTRAIT_FLIPPED; + break; + case UIInterfaceOrientationLandscapeLeft: + // Bug: UIInterfaceOrientationLandscapeLeft/Right are reversed - http://openradar.appspot.com/7216046 + orientation = SDL_ORIENTATION_LANDSCAPE_FLIPPED; + break; + case UIInterfaceOrientationLandscapeRight: + // Bug: UIInterfaceOrientationLandscapeLeft/Right are reversed - http://openradar.appspot.com/7216046 + orientation = SDL_ORIENTATION_LANDSCAPE; + break; + default: + break; + } + SDL_SendDisplayEvent(display, SDL_EVENT_DISPLAY_ORIENTATION, orientation, 0); + } +} +#endif // !SDL_PLATFORM_TVOS + +#endif // SDL_VIDEO_DRIVER_UIKIT diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopengles.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopengles.h new file mode 100644 index 0000000..cc259f7 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopengles.h @@ -0,0 +1,40 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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_uikitopengles_ +#define SDL_uikitopengles_ + +#if defined(SDL_VIDEO_OPENGL_ES) || defined(SDL_VIDEO_OPENGL_ES2) + +#include "../SDL_sysvideo.h" + +extern bool UIKit_GL_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, + SDL_GLContext context); +extern bool UIKit_GL_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern SDL_GLContext UIKit_GL_CreateContext(SDL_VideoDevice *_this, SDL_Window *window); +extern bool UIKit_GL_DestroyContext(SDL_VideoDevice *_this, SDL_GLContext context); +extern SDL_FunctionPointer UIKit_GL_GetProcAddress(SDL_VideoDevice *_this, const char *proc); +extern bool UIKit_GL_LoadLibrary(SDL_VideoDevice *_this, const char *path); + +extern void UIKit_GL_RestoreCurrentContext(void); + +#endif // SDL_VIDEO_OPENGL_ES || SDL_VIDEO_OPENGL_ES2 + +#endif // SDL_uikitopengles_ diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopengles.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopengles.m new file mode 100644 index 0000000..a73588b --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopengles.m @@ -0,0 +1,221 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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" + +#if defined(SDL_VIDEO_DRIVER_UIKIT) && (defined(SDL_VIDEO_OPENGL_ES) || defined(SDL_VIDEO_OPENGL_ES2)) + +#include "SDL_uikitopengles.h" +#import "SDL_uikitopenglview.h" +#include "SDL_uikitmodes.h" +#include "SDL_uikitwindow.h" +#include "SDL_uikitevents.h" +#include "../SDL_sysvideo.h" +#include "../../events/SDL_keyboard_c.h" +#include "../../events/SDL_mouse_c.h" +#include "../../power/uikit/SDL_syspower.h" +#include + +@interface SDLEAGLContext : EAGLContext + +// The OpenGL ES context owns a view / drawable. +@property(nonatomic, strong) SDL_uikitopenglview *sdlView; + +@end + +@implementation SDLEAGLContext + +- (void)dealloc +{ + /* When the context is deallocated, its view should be removed from any + * SDL window that it's attached to. */ + [self.sdlView setSDLWindow:NULL]; +} + +@end + +SDL_FunctionPointer UIKit_GL_GetProcAddress(SDL_VideoDevice *_this, const char *proc) +{ + /* Look through all SO's for the proc symbol. Here's why: + * -Looking for the path to the OpenGL Library seems not to work in the iOS Simulator. + * -We don't know that the path won't change in the future. */ + return dlsym(RTLD_DEFAULT, proc); +} + +/* + note that SDL_GL_DestroyContext makes it current without passing the window +*/ +bool UIKit_GL_MakeCurrent(SDL_VideoDevice *_this, SDL_Window *window, SDL_GLContext context) +{ + @autoreleasepool { + SDLEAGLContext *eaglcontext = (__bridge SDLEAGLContext *)context; + + if (![EAGLContext setCurrentContext:eaglcontext]) { + return SDL_SetError("Could not make EAGL context current"); + } + + if (eaglcontext) { + [eaglcontext.sdlView setSDLWindow:window]; + } + } + + return true; +} + +bool UIKit_GL_LoadLibrary(SDL_VideoDevice *_this, const char *path) +{ + /* We shouldn't pass a path to this function, since we've already loaded the + * library. */ + if (path != NULL) { + return SDL_SetError("iOS GL Load Library just here for compatibility"); + } + return true; +} + +bool UIKit_GL_SwapWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDLEAGLContext *context = (__bridge SDLEAGLContext *)SDL_GL_GetCurrentContext(); + +#ifdef SDL_POWER_UIKIT + // Check once a frame to see if we should turn off the battery monitor. + SDL_UIKit_UpdateBatteryMonitoring(); +#endif + + [context.sdlView swapBuffers]; + + /* You need to pump events in order for the OS to make changes visible. + * We don't pump events here because we don't want iOS application events + * (low memory, terminate, etc.) to happen inside low level rendering. */ + } + return true; +} + +SDL_GLContext UIKit_GL_CreateContext(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDLEAGLContext *context = nil; + SDL_uikitopenglview *view; + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + CGRect frame = UIKit_ComputeViewFrame(window, data.uiwindow.screen); + EAGLSharegroup *sharegroup = nil; + CGFloat scale = 1.0; + int samples = 0; + int major = _this->gl_config.major_version; + int minor = _this->gl_config.minor_version; + + /* The EAGLRenderingAPI enum values currently map 1:1 to major GLES + * versions. */ + EAGLRenderingAPI api = major; + + // iOS currently doesn't support GLES >3.0. + if (major > 3 || (major == 3 && minor > 0)) { + SDL_SetError("OpenGL ES %d.%d context could not be created", major, minor); + return NULL; + } + + if (_this->gl_config.multisamplebuffers > 0) { + samples = _this->gl_config.multisamplesamples; + } + + if (_this->gl_config.share_with_current_context) { + EAGLContext *currContext = (__bridge EAGLContext *)SDL_GL_GetCurrentContext(); + sharegroup = currContext.sharegroup; + } + + if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) { + /* Set the scale to the natural scale factor of the screen - the + * backing dimensions of the OpenGL view will match the pixel + * dimensions of the screen rather than the dimensions in points. */ + scale = data.uiwindow.screen.nativeScale; + } + + context = [[SDLEAGLContext alloc] initWithAPI:api sharegroup:sharegroup]; + if (!context) { + SDL_SetError("OpenGL ES %d context could not be created", _this->gl_config.major_version); + return NULL; + } + + // construct our view, passing in SDL's OpenGL configuration data + view = [[SDL_uikitopenglview alloc] initWithFrame:frame + scale:scale + retainBacking:_this->gl_config.retained_backing + rBits:_this->gl_config.red_size + gBits:_this->gl_config.green_size + bBits:_this->gl_config.blue_size + aBits:_this->gl_config.alpha_size + depthBits:_this->gl_config.depth_size + stencilBits:_this->gl_config.stencil_size + sRGB:_this->gl_config.framebuffer_srgb_capable + multisamples:samples + context:context]; + + if (!view) { + return NULL; + } + + SDL_PropertiesID props = SDL_GetWindowProperties(window); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_UIKIT_OPENGL_FRAMEBUFFER_NUMBER, view.drawableFramebuffer); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_UIKIT_OPENGL_RENDERBUFFER_NUMBER, view.drawableRenderbuffer); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_UIKIT_OPENGL_RESOLVE_FRAMEBUFFER_NUMBER, view.msaaResolveFramebuffer); + + // The context owns the view / drawable. + context.sdlView = view; + + if (!UIKit_GL_MakeCurrent(_this, window, (__bridge SDL_GLContext)context)) { + UIKit_GL_DestroyContext(_this, (SDL_GLContext)CFBridgingRetain(context)); + return NULL; + } + + /* We return a +1'd context. The window's internal owns the view (via + * MakeCurrent.) */ + return (SDL_GLContext)CFBridgingRetain(context); + } +} + +bool UIKit_GL_DestroyContext(SDL_VideoDevice *_this, SDL_GLContext context) +{ + @autoreleasepool { + /* The context was retained in SDL_GL_CreateContext, so we release it + * here. The context's view will be detached from its window when the + * context is deallocated. */ + CFRelease(context); + } + return true; +} + +void UIKit_GL_RestoreCurrentContext(void) +{ + @autoreleasepool { + /* Some iOS system functionality (such as Dictation on the on-screen + keyboard) uses its own OpenGL ES context but doesn't restore the + previous one when it's done. This is a workaround to make sure the + expected SDL-created OpenGL ES context is active after the OS is + finished running its own code for the frame. If this isn't done, the + app may crash or have other nasty symptoms when Dictation is used. + */ + EAGLContext *context = (__bridge EAGLContext *)SDL_GL_GetCurrentContext(); + if (context != NULL && [EAGLContext currentContext] != context) { + [EAGLContext setCurrentContext:context]; + } + } +} + +#endif // SDL_VIDEO_DRIVER_UIKIT diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopenglview.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopenglview.h new file mode 100644 index 0000000..3286a16 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopenglview.h @@ -0,0 +1,62 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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. +*/ + +#if defined(SDL_VIDEO_OPENGL_ES) || defined(SDL_VIDEO_OPENGL_ES2) + +#import +#import +#import + +#import "SDL_uikitview.h" +#include "SDL_uikitvideo.h" + +@interface SDL_uikitopenglview : SDL_uikitview + +- (instancetype)initWithFrame:(CGRect)frame + scale:(CGFloat)scale + retainBacking:(BOOL)retained + rBits:(int)rBits + gBits:(int)gBits + bBits:(int)bBits + aBits:(int)aBits + depthBits:(int)depthBits + stencilBits:(int)stencilBits + sRGB:(BOOL)sRGB + multisamples:(int)multisamples + context:(EAGLContext *)glcontext; + +@property(nonatomic, readonly, weak) EAGLContext *context; + +// The width and height of the drawable in pixels (as opposed to points.) +@property(nonatomic, readonly) int backingWidth; +@property(nonatomic, readonly) int backingHeight; + +@property(nonatomic, readonly) GLuint drawableRenderbuffer; +@property(nonatomic, readonly) GLuint drawableFramebuffer; +@property(nonatomic, readonly) GLuint msaaResolveFramebuffer; + +- (void)swapBuffers; + +- (void)updateFrame; + +@end + +#endif // SDL_VIDEO_OPENGL_ES || SDL_VIDEO_OPENGL_ES2 diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopenglview.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopenglview.m new file mode 100644 index 0000000..71d167f --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitopenglview.m @@ -0,0 +1,377 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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" + +#if defined(SDL_VIDEO_DRIVER_UIKIT) && (defined(SDL_VIDEO_OPENGL_ES) || defined(SDL_VIDEO_OPENGL_ES2)) + +#include +#include +#import "SDL_uikitopenglview.h" +#include "SDL_uikitwindow.h" + +@implementation SDL_uikitopenglview +{ + // The renderbuffer and framebuffer used to render to this layer. + GLuint viewRenderbuffer, viewFramebuffer; + + // The depth buffer that is attached to viewFramebuffer, if it exists. + GLuint depthRenderbuffer; + + GLenum colorBufferFormat; + + // format of depthRenderbuffer + GLenum depthBufferFormat; + + // The framebuffer and renderbuffer used for rendering with MSAA. + GLuint msaaFramebuffer, msaaRenderbuffer; + + // The number of MSAA samples. + int samples; + + BOOL retainedBacking; +} + +@synthesize context; +@synthesize backingWidth; +@synthesize backingHeight; + ++ (Class)layerClass +{ + return [CAEAGLLayer class]; +} + +- (instancetype)initWithFrame:(CGRect)frame + scale:(CGFloat)scale + retainBacking:(BOOL)retained + rBits:(int)rBits + gBits:(int)gBits + bBits:(int)bBits + aBits:(int)aBits + depthBits:(int)depthBits + stencilBits:(int)stencilBits + sRGB:(BOOL)sRGB + multisamples:(int)multisamples + context:(EAGLContext *)glcontext +{ + if ((self = [super initWithFrame:frame])) { + const BOOL useStencilBuffer = (stencilBits != 0); + const BOOL useDepthBuffer = (depthBits != 0); + NSString *colorFormat = nil; + + context = glcontext; + samples = multisamples; + retainedBacking = retained; + + if (!context || ![EAGLContext setCurrentContext:context]) { + SDL_SetError("Could not create OpenGL ES drawable (could not make context current)"); + return nil; + } + + if (samples > 0) { + GLint maxsamples = 0; + glGetIntegerv(GL_MAX_SAMPLES, &maxsamples); + + // Clamp the samples to the max supported count. + samples = SDL_min(samples, maxsamples); + } + + if (sRGB) { + colorFormat = kEAGLColorFormatSRGBA8; + colorBufferFormat = GL_SRGB8_ALPHA8; + } else if (rBits >= 8 || gBits >= 8 || bBits >= 8 || aBits > 0) { + // if user specifically requests rbg888 or some color format higher than 16bpp + colorFormat = kEAGLColorFormatRGBA8; + colorBufferFormat = GL_RGBA8; + } else { + // default case (potentially faster) + colorFormat = kEAGLColorFormatRGB565; + colorBufferFormat = GL_RGB565; + } + + CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer; + + eaglLayer.opaque = YES; + eaglLayer.drawableProperties = @{ + kEAGLDrawablePropertyRetainedBacking : @(retained), + kEAGLDrawablePropertyColorFormat : colorFormat + }; + + // Set the appropriate scale (for retina display support) + self.contentScaleFactor = scale; + + // Create the color Renderbuffer Object + glGenRenderbuffers(1, &viewRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, viewRenderbuffer); + + if (![context renderbufferStorage:GL_RENDERBUFFER fromDrawable:eaglLayer]) { + SDL_SetError("Failed to create OpenGL ES drawable"); + return nil; + } + + // Create the Framebuffer Object + glGenFramebuffers(1, &viewFramebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, viewFramebuffer); + + // attach the color renderbuffer to the FBO + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, viewRenderbuffer); + + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth); + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + SDL_SetError("Failed creating OpenGL ES framebuffer"); + return nil; + } + + /* When MSAA is used we'll use a separate framebuffer for rendering to, + * since we'll need to do an explicit MSAA resolve before presenting. */ + if (samples > 0) { + glGenFramebuffers(1, &msaaFramebuffer); + glBindFramebuffer(GL_FRAMEBUFFER, msaaFramebuffer); + + glGenRenderbuffers(1, &msaaRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, msaaRenderbuffer); + + glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, colorBufferFormat, backingWidth, backingHeight); + + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msaaRenderbuffer); + } + + if (useDepthBuffer || useStencilBuffer) { + if (useStencilBuffer) { + // Apparently you need to pack stencil and depth into one buffer. + depthBufferFormat = GL_DEPTH24_STENCIL8_OES; + } else if (useDepthBuffer) { + /* iOS only uses 32-bit float (exposed as fixed point 24-bit) + * depth buffers. */ + depthBufferFormat = GL_DEPTH_COMPONENT24_OES; + } + + glGenRenderbuffers(1, &depthRenderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer); + + if (samples > 0) { + glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, depthBufferFormat, backingWidth, backingHeight); + } else { + glRenderbufferStorage(GL_RENDERBUFFER, depthBufferFormat, backingWidth, backingHeight); + } + + if (useDepthBuffer) { + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer); + } + if (useStencilBuffer) { + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer); + } + } + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { + SDL_SetError("Failed creating OpenGL ES framebuffer"); + return nil; + } + + glBindRenderbuffer(GL_RENDERBUFFER, viewRenderbuffer); + + [self setDebugLabels]; + } + + return self; +} + +- (GLuint)drawableRenderbuffer +{ + return viewRenderbuffer; +} + +- (GLuint)drawableFramebuffer +{ + // When MSAA is used, the MSAA draw framebuffer is used for drawing. + if (msaaFramebuffer) { + return msaaFramebuffer; + } else { + return viewFramebuffer; + } +} + +- (GLuint)msaaResolveFramebuffer +{ + /* When MSAA is used, the MSAA draw framebuffer is used for drawing and the + * view framebuffer is used as a MSAA resolve framebuffer. */ + if (msaaFramebuffer) { + return viewFramebuffer; + } else { + return 0; + } +} + +- (void)updateFrame +{ + GLint prevRenderbuffer = 0; + glGetIntegerv(GL_RENDERBUFFER_BINDING, &prevRenderbuffer); + + glBindRenderbuffer(GL_RENDERBUFFER, viewRenderbuffer); + [context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer]; + + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth); + glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight); + + if (msaaRenderbuffer != 0) { + glBindRenderbuffer(GL_RENDERBUFFER, msaaRenderbuffer); + glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, colorBufferFormat, backingWidth, backingHeight); + } + + if (depthRenderbuffer != 0) { + glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer); + + if (samples > 0) { + glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, depthBufferFormat, backingWidth, backingHeight); + } else { + glRenderbufferStorage(GL_RENDERBUFFER, depthBufferFormat, backingWidth, backingHeight); + } + } + + glBindRenderbuffer(GL_RENDERBUFFER, prevRenderbuffer); +} + +- (void)setDebugLabels +{ + if (viewFramebuffer != 0) { + glLabelObjectEXT(GL_FRAMEBUFFER, viewFramebuffer, 0, "context FBO"); + } + + if (viewRenderbuffer != 0) { + glLabelObjectEXT(GL_RENDERBUFFER, viewRenderbuffer, 0, "context color buffer"); + } + + if (depthRenderbuffer != 0) { + if (depthBufferFormat == GL_DEPTH24_STENCIL8_OES) { + glLabelObjectEXT(GL_RENDERBUFFER, depthRenderbuffer, 0, "context depth-stencil buffer"); + } else { + glLabelObjectEXT(GL_RENDERBUFFER, depthRenderbuffer, 0, "context depth buffer"); + } + } + + if (msaaFramebuffer != 0) { + glLabelObjectEXT(GL_FRAMEBUFFER, msaaFramebuffer, 0, "context MSAA FBO"); + } + + if (msaaRenderbuffer != 0) { + glLabelObjectEXT(GL_RENDERBUFFER, msaaRenderbuffer, 0, "context MSAA renderbuffer"); + } +} + +- (void)swapBuffers +{ + if (msaaFramebuffer) { + const GLenum attachments[] = { GL_COLOR_ATTACHMENT0 }; + + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, viewFramebuffer); + + /* OpenGL ES 3+ provides explicit MSAA resolves via glBlitFramebuffer. + * In OpenGL ES 1 and 2, MSAA resolves must be done via an extension. */ + if (context.API >= kEAGLRenderingAPIOpenGLES3) { + int w = backingWidth; + int h = backingHeight; + glBlitFramebuffer(0, 0, w, h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST); + + if (!retainedBacking) { + // Discard the contents of the MSAA drawable color buffer. + glInvalidateFramebuffer(GL_READ_FRAMEBUFFER, 1, attachments); + } + } else { + glResolveMultisampleFramebufferAPPLE(); + + if (!retainedBacking) { + glDiscardFramebufferEXT(GL_READ_FRAMEBUFFER, 1, attachments); + } + } + + /* We assume the "drawable framebuffer" (MSAA draw framebuffer) was + * previously bound... */ + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, msaaFramebuffer); + } + + /* viewRenderbuffer should always be bound here. Code that binds something + * else is responsible for rebinding viewRenderbuffer, to reduce duplicate + * state changes. */ + [context presentRenderbuffer:GL_RENDERBUFFER]; +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + int width = (int)(self.bounds.size.width * self.contentScaleFactor); + int height = (int)(self.bounds.size.height * self.contentScaleFactor); + + // Update the color and depth buffer storage if the layer size has changed. + if (width != backingWidth || height != backingHeight) { + EAGLContext *prevContext = [EAGLContext currentContext]; + if (prevContext != context) { + [EAGLContext setCurrentContext:context]; + } + + [self updateFrame]; + + if (prevContext != context) { + [EAGLContext setCurrentContext:prevContext]; + } + } +} + +- (void)destroyFramebuffer +{ + if (viewFramebuffer != 0) { + glDeleteFramebuffers(1, &viewFramebuffer); + viewFramebuffer = 0; + } + + if (viewRenderbuffer != 0) { + glDeleteRenderbuffers(1, &viewRenderbuffer); + viewRenderbuffer = 0; + } + + if (depthRenderbuffer != 0) { + glDeleteRenderbuffers(1, &depthRenderbuffer); + depthRenderbuffer = 0; + } + + if (msaaFramebuffer != 0) { + glDeleteFramebuffers(1, &msaaFramebuffer); + msaaFramebuffer = 0; + } + + if (msaaRenderbuffer != 0) { + glDeleteRenderbuffers(1, &msaaRenderbuffer); + msaaRenderbuffer = 0; + } +} + +- (void)dealloc +{ + if (context && context == [EAGLContext currentContext]) { + [self destroyFramebuffer]; + [EAGLContext setCurrentContext:nil]; + } +} + +@end + +#endif // SDL_VIDEO_DRIVER_UIKIT diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitpen.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitpen.h new file mode 100644 index 0000000..1e75589 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitpen.h @@ -0,0 +1,39 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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_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) +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/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitpen.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitpen.m new file mode 100644 index 0000000..5209681 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitpen.m @@ -0,0 +1,214 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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" + +// Fix build errors when using an older SDK by defining these selectors +#if !defined(SDL_PLATFORM_TVOS) + +@interface UITouch (SDL) +#if !(__IPHONE_OS_VERSION_MAX_ALLOWED >= 170500) +@property (nonatomic, readonly) CGFloat rollAngle; +#endif +@end + +@interface UIHoverGestureRecognizer (SDL) +#if !(__IPHONE_OS_VERSION_MAX_ALLOWED >= 160100) +@property (nonatomic, readonly) CGFloat zOffset; +#endif +#if !(__IPHONE_OS_VERSION_MAX_ALLOWED >= 160400) +- (CGFloat) azimuthAngleInView:(UIView *) view; + +@property (nonatomic, readonly) CGFloat altitudeAngle; +#endif +#if !(__IPHONE_OS_VERSION_MAX_ALLOWED >= 170500) +@property (nonatomic, readonly) CGFloat rollAngle; +#endif +@end + +#endif // !SDL_PLATFORM_TVOS + +static SDL_PenID apple_pencil_id = 0; + +bool UIKit_InitPen(SDL_VideoDevice *_this) +{ + return true; +} + +// 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() +{ + 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; + + 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; +} + +static void UIKit_HandlePenAxes(SDL_Window *window, NSTimeInterval nstimestamp, float zOffset, const CGPoint *point, float force, + float maximumPossibleForce, float azimuthAngleInView, float altitudeAngle, float rollAngle) +{ + 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) +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) +{ + UIKit_HandlePenAxesFromUITouch(view, pencil); +} + +void UIKit_HandlePenPress(SDL_uikitview *view, UITouch *pencil) +{ + 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) +{ + 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/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvideo.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvideo.h new file mode 100644 index 0000000..927e646 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvideo.h @@ -0,0 +1,52 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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_uikitvideo_h_ +#define SDL_uikitvideo_h_ + +#include "../SDL_sysvideo.h" + +#ifdef __OBJC__ + +#include + +@interface SDL_UIKitVideoData : NSObject + +@property(nonatomic, assign) id pasteboardObserver; + +@end + +#ifdef SDL_PLATFORM_VISIONOS +CGRect UIKit_ComputeViewFrame(SDL_Window *window); +#else +CGRect UIKit_ComputeViewFrame(SDL_Window *window, UIScreen *screen); +#endif + +#endif // __OBJC__ + +bool UIKit_SuspendScreenSaver(SDL_VideoDevice *_this); + +void UIKit_ForceUpdateHomeIndicator(void); + +bool UIKit_IsSystemVersionAtLeast(double version); + +SDL_SystemTheme UIKit_GetSystemTheme(void); + +#endif // SDL_uikitvideo_h_ diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvideo.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvideo.m new file mode 100644 index 0000000..5c3987d --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvideo.m @@ -0,0 +1,312 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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 + +#import + +#include "../SDL_sysvideo.h" +#include "../SDL_pixels_c.h" +#include "../../events/SDL_events_c.h" + +#include "SDL_uikitvideo.h" +#include "SDL_uikitevents.h" +#include "SDL_uikitmodes.h" +#include "SDL_uikitwindow.h" +#include "SDL_uikitopengles.h" +#include "SDL_uikitclipboard.h" +#include "SDL_uikitvulkan.h" +#include "SDL_uikitmetalview.h" +#include "SDL_uikitmessagebox.h" + +#define UIKITVID_DRIVER_NAME "uikit" + +@implementation SDL_UIKitVideoData + +@end + +// Initialization/Query functions +static bool UIKit_VideoInit(SDL_VideoDevice *_this); +static void UIKit_VideoQuit(SDL_VideoDevice *_this); + +// DUMMY driver bootstrap functions + +static void UIKit_DeleteDevice(SDL_VideoDevice *device) +{ + @autoreleasepool { + if (device->internal){ + CFRelease(device->internal); + } + SDL_free(device); + } +} + +static SDL_VideoDevice *UIKit_CreateDevice(void) +{ + @autoreleasepool { + SDL_VideoDevice *device; + SDL_UIKitVideoData *data; + + // Initialize all variables that we clean on shutdown + device = (SDL_VideoDevice *)SDL_calloc(1, sizeof(SDL_VideoDevice)); + if (!device) { + return NULL; + } + + data = [SDL_UIKitVideoData new]; + + device->internal = (SDL_VideoData *)CFBridgingRetain(data); + device->system_theme = UIKit_GetSystemTheme(); + + // Set the function pointers + device->VideoInit = UIKit_VideoInit; + device->VideoQuit = UIKit_VideoQuit; + device->GetDisplayModes = UIKit_GetDisplayModes; + device->SetDisplayMode = UIKit_SetDisplayMode; + device->PumpEvents = UIKit_PumpEvents; + device->SuspendScreenSaver = UIKit_SuspendScreenSaver; + device->CreateSDLWindow = UIKit_CreateWindow; + device->SetWindowTitle = UIKit_SetWindowTitle; + device->ShowWindow = UIKit_ShowWindow; + device->HideWindow = UIKit_HideWindow; + device->RaiseWindow = UIKit_RaiseWindow; + device->SetWindowBordered = UIKit_SetWindowBordered; + device->SetWindowFullscreen = UIKit_SetWindowFullscreen; + device->DestroyWindow = UIKit_DestroyWindow; + device->GetDisplayUsableBounds = UIKit_GetDisplayUsableBounds; + device->GetWindowSizeInPixels = UIKit_GetWindowSizeInPixels; + +#ifdef SDL_IPHONE_KEYBOARD + device->HasScreenKeyboardSupport = UIKit_HasScreenKeyboardSupport; + device->StartTextInput = UIKit_StartTextInput; + device->StopTextInput = UIKit_StopTextInput; + device->SetTextInputProperties = UIKit_SetTextInputProperties; + device->IsScreenKeyboardShown = UIKit_IsScreenKeyboardShown; + device->UpdateTextInputArea = UIKit_UpdateTextInputArea; +#endif + + device->SetClipboardText = UIKit_SetClipboardText; + device->GetClipboardText = UIKit_GetClipboardText; + device->HasClipboardText = UIKit_HasClipboardText; + + // OpenGL (ES) functions +#if defined(SDL_VIDEO_OPENGL_ES) || defined(SDL_VIDEO_OPENGL_ES2) + device->GL_MakeCurrent = UIKit_GL_MakeCurrent; + device->GL_SwapWindow = UIKit_GL_SwapWindow; + device->GL_CreateContext = UIKit_GL_CreateContext; + device->GL_DestroyContext = UIKit_GL_DestroyContext; + device->GL_GetProcAddress = UIKit_GL_GetProcAddress; + device->GL_LoadLibrary = UIKit_GL_LoadLibrary; +#endif + device->free = UIKit_DeleteDevice; + +#ifdef SDL_VIDEO_VULKAN + device->Vulkan_LoadLibrary = UIKit_Vulkan_LoadLibrary; + device->Vulkan_UnloadLibrary = UIKit_Vulkan_UnloadLibrary; + device->Vulkan_GetInstanceExtensions = UIKit_Vulkan_GetInstanceExtensions; + device->Vulkan_CreateSurface = UIKit_Vulkan_CreateSurface; + device->Vulkan_DestroySurface = UIKit_Vulkan_DestroySurface; +#endif + +#ifdef SDL_VIDEO_METAL + device->Metal_CreateView = UIKit_Metal_CreateView; + device->Metal_DestroyView = UIKit_Metal_DestroyView; + device->Metal_GetLayer = UIKit_Metal_GetLayer; +#endif + + device->device_caps = VIDEO_DEVICE_CAPS_SENDS_FULLSCREEN_DIMENSIONS; + + device->gl_config.accelerated = 1; + + return device; + } +} + +VideoBootStrap UIKIT_bootstrap = { + UIKITVID_DRIVER_NAME, "SDL UIKit video driver", + UIKit_CreateDevice, + UIKit_ShowMessageBox, + false +}; + +static bool UIKit_VideoInit(SDL_VideoDevice *_this) +{ + _this->gl_config.driver_loaded = 1; + + if (!UIKit_InitModes(_this)) { + return false; + } + + SDL_InitGCKeyboard(); + SDL_InitGCMouse(); + + UIKit_InitClipboard(_this); + + return true; +} + +static void UIKit_VideoQuit(SDL_VideoDevice *_this) +{ + UIKit_QuitClipboard(_this); + + SDL_QuitGCKeyboard(); + SDL_QuitGCMouse(); + + UIKit_QuitModes(_this); +} + +bool UIKit_SuspendScreenSaver(SDL_VideoDevice *_this) +{ + @autoreleasepool { + UIApplication *app = [UIApplication sharedApplication]; + + // Prevent the display from dimming and going to sleep. + app.idleTimerDisabled = (_this->suspend_screensaver != false); + } + return true; +} + +bool UIKit_IsSystemVersionAtLeast(double version) +{ + return [[UIDevice currentDevice].systemVersion doubleValue] >= version; +} + +SDL_SystemTheme UIKit_GetSystemTheme(void) +{ +#ifndef SDL_PLATFORM_VISIONOS + if (@available(iOS 12.0, tvOS 10.0, *)) { + switch ([UIScreen mainScreen].traitCollection.userInterfaceStyle) { + case UIUserInterfaceStyleDark: + return SDL_SYSTEM_THEME_DARK; + case UIUserInterfaceStyleLight: + return SDL_SYSTEM_THEME_LIGHT; + default: + break; + } + } +#endif + return SDL_SYSTEM_THEME_UNKNOWN; +} + +#ifdef SDL_PLATFORM_VISIONOS +CGRect UIKit_ComputeViewFrame(SDL_Window *window){ + return CGRectMake(window->x, window->y, window->w, window->h); +} +#else +CGRect UIKit_ComputeViewFrame(SDL_Window *window, UIScreen *screen) +{ + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + CGRect frame = screen.bounds; + + /* Use the UIWindow bounds instead of the UIScreen bounds, when possible. + * The uiwindow bounds may be smaller than the screen bounds when Split View + * is used on an iPad. */ + if (data != nil && data.uiwindow != nil) { + frame = data.uiwindow.bounds; + } + +#ifndef SDL_PLATFORM_TVOS + /* iOS 10 seems to have a bug where, in certain conditions, putting the + * device to sleep with the a landscape-only app open, re-orienting the + * device to portrait, and turning it back on will result in the screen + * bounds returning portrait orientation despite the app being in landscape. + * This is a workaround until a better solution can be found. + * https://bugzilla.libsdl.org/show_bug.cgi?id=3505 + * https://bugzilla.libsdl.org/show_bug.cgi?id=3465 + * https://forums.developer.apple.com/thread/65337 */ + UIInterfaceOrientation orient = [UIApplication sharedApplication].statusBarOrientation; + BOOL landscape = UIInterfaceOrientationIsLandscape(orient) || + !(UIKit_GetSupportedOrientations(window) & (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown)); + BOOL fullscreen = CGRectEqualToRect(screen.bounds, frame); + + /* The orientation flip doesn't make sense when the window is smaller + * than the screen (iPad Split View, for example). */ + if (fullscreen && (landscape != (frame.size.width > frame.size.height))) { + float height = frame.size.width; + frame.size.width = frame.size.height; + frame.size.height = height; + } +#endif + + return frame; +} + +#endif + +void UIKit_ForceUpdateHomeIndicator(void) +{ +#ifndef SDL_PLATFORM_TVOS + // Force the main SDL window to re-evaluate home indicator state + SDL_Window *focus = SDL_GetKeyboardFocus(); + if (focus) { + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)focus->internal; + if (data != nil) { + [data.viewcontroller performSelectorOnMainThread:@selector(setNeedsUpdateOfHomeIndicatorAutoHidden) withObject:nil waitUntilDone:NO]; + [data.viewcontroller performSelectorOnMainThread:@selector(setNeedsUpdateOfScreenEdgesDeferringSystemGestures) withObject:nil waitUntilDone:NO]; + } + } +#endif // !SDL_PLATFORM_TVOS +} + +/* + * iOS log support. + * + * This doesn't really have anything to do with the interfaces of the SDL video + * subsystem, but we need to stuff this into an Objective-C source code file. + * + * NOTE: This is copypasted from src/video/cocoa/SDL_cocoavideo.m! Thus, if + * Cocoa is supported, we use that one instead. Be sure both versions remain + * identical! + */ + +#ifndef SDL_VIDEO_DRIVER_COCOA +void SDL_NSLog(const char *prefix, const char *text) +{ + @autoreleasepool { + NSString *nsText = [NSString stringWithUTF8String:text]; + if (prefix && *prefix) { + NSString *nsPrefix = [NSString stringWithUTF8String:prefix]; + NSLog(@"%@%@", nsPrefix, nsText); + } else { + NSLog(@"%@", nsText); + } + } +} +#endif // SDL_VIDEO_DRIVER_COCOA + +/* + * iOS Tablet, etc, detection + * + * This doesn't really have anything to do with the interfaces of the SDL video + * subsystem, but we need to stuff this into an Objective-C source code file. + */ +bool SDL_IsIPad(void) +{ + return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad); +} + +bool SDL_IsAppleTV(void) +{ + return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomTV); +} + +#endif // SDL_VIDEO_DRIVER_UIKIT diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitview.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitview.h new file mode 100644 index 0000000..78c2bed --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitview.h @@ -0,0 +1,52 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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. +*/ + +#import + +#include "../SDL_sysvideo.h" + +#if !defined(SDL_PLATFORM_TVOS) +@interface SDL_uikitview : UIView +#else +@interface SDL_uikitview : UIView +#endif + +- (instancetype)initWithFrame:(CGRect)frame; + +- (void)setSDLWindow:(SDL_Window *)window; +- (SDL_Window *)getSDLWindow; + +#if !defined(SDL_PLATFORM_TVOS) +- (void)pencilHovering:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0)); + +- (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)); +#endif + +- (CGPoint)touchLocation:(UITouch *)touch shouldNormalize:(BOOL)normalize; +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event; +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event; + +- (void)safeAreaInsetsDidChange; + +@end diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitview.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitview.m new file mode 100644 index 0000000..ba3b09b --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitview.m @@ -0,0 +1,587 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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_uikitview.h" + +#include "../../events/SDL_mouse_c.h" +#include "../../events/SDL_touch_c.h" +#include "../../events/SDL_events_c.h" + +#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 +#define MAX_MOUSE_BUTTONS 5 + +// This is defined in SDL_sysjoystick.m +#ifndef SDL_JOYSTICK_DISABLED +extern int SDL_AppleTVRemoteOpenedAsJoystick; +#endif + +@implementation SDL_uikitview +{ + SDL_Window *sdlwindow; + + SDL_TouchID directTouchId; + SDL_TouchID indirectTouchId; + +#if !defined(SDL_PLATFORM_TVOS) + UIPointerInteraction *indirectPointerInteraction API_AVAILABLE(ios(13.4)); +#endif +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if ((self = [super initWithFrame:frame])) { +#ifdef SDL_PLATFORM_TVOS + // Apple TV Remote touchpad swipe gestures. + UISwipeGestureRecognizer *swipeUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeGesture:)]; + swipeUp.direction = UISwipeGestureRecognizerDirectionUp; + [self addGestureRecognizer:swipeUp]; + + UISwipeGestureRecognizer *swipeDown = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeGesture:)]; + swipeDown.direction = UISwipeGestureRecognizerDirectionDown; + [self addGestureRecognizer:swipeDown]; + + UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeGesture:)]; + swipeLeft.direction = UISwipeGestureRecognizerDirectionLeft; + [self addGestureRecognizer:swipeLeft]; + + UISwipeGestureRecognizer *swipeRight = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeGesture:)]; + swipeRight.direction = UISwipeGestureRecognizerDirectionRight; + [self addGestureRecognizer:swipeRight]; +#endif + + self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.autoresizesSubviews = YES; + + directTouchId = 1; + indirectTouchId = 2; + +#ifndef SDL_PLATFORM_TVOS + self.multipleTouchEnabled = YES; + SDL_AddTouch(directTouchId, SDL_TOUCH_DEVICE_DIRECT, ""); + + if (@available(iOS 13.0, *)) { + UIHoverGestureRecognizer *pencilRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(pencilHovering:)]; + pencilRecognizer.allowedTouchTypes = @[@(UITouchTypePencil)]; + [self addGestureRecognizer:pencilRecognizer]; + } + + if (@available(iOS 13.4, *)) { + indirectPointerInteraction = [[UIPointerInteraction alloc] initWithDelegate:self]; + [self addInteraction:indirectPointerInteraction]; + + UIHoverGestureRecognizer *indirectPointerRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(indirectPointerHovering:)]; + indirectPointerRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)]; + [self addGestureRecognizer:indirectPointerRecognizer]; + } +#endif // !defined(SDL_PLATFORM_TVOS) + } + + return self; +} + +- (void)setSDLWindow:(SDL_Window *)window +{ + SDL_UIKitWindowData *data = nil; + + if (window == sdlwindow) { + return; + } + + // Remove ourself from the old window. + if (sdlwindow) { + SDL_uikitview *view = nil; + data = (__bridge SDL_UIKitWindowData *)sdlwindow->internal; + + [data.views removeObject:self]; + + [self removeFromSuperview]; + + // Restore the next-oldest view in the old window. + view = data.views.lastObject; + + data.viewcontroller.view = view; + + data.uiwindow.rootViewController = nil; + data.uiwindow.rootViewController = data.viewcontroller; + + [data.uiwindow layoutIfNeeded]; + } + + sdlwindow = window; + + // Add ourself to the new window. + if (window) { + data = (__bridge SDL_UIKitWindowData *)window->internal; + + // Make sure the SDL window has a strong reference to this view. + [data.views addObject:self]; + + // Replace the view controller's old view with this one. + [data.viewcontroller.view removeFromSuperview]; + data.viewcontroller.view = self; + + /* The root view controller handles rotation and the status bar. + * Assigning it also adds the controller's view to the window. We + * explicitly re-set it to make sure the view is properly attached to + * the window. Just adding the sub-view if the root view controller is + * already correct causes orientation issues on iOS 7 and below. */ + data.uiwindow.rootViewController = nil; + data.uiwindow.rootViewController = data.viewcontroller; + + /* The view's bounds may not be correct until the next event cycle. That + * might happen after the current dimensions are queried, so we force a + * layout now to immediately update the bounds. */ + [data.uiwindow layoutIfNeeded]; + } +} + +- (SDL_Window *)getSDLWindow +{ + return sdlwindow; +} + +#if !defined(SDL_PLATFORM_TVOS) + +- (UIPointerRegion *)pointerInteraction:(UIPointerInteraction *)interaction regionForRequest:(UIPointerRegionRequest *)request defaultRegion:(UIPointerRegion *)defaultRegion API_AVAILABLE(ios(13.4)) +{ + return [UIPointerRegion regionWithRect:self.bounds identifier:nil]; +} + +- (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction styleForRegion:(UIPointerRegion *)region API_AVAILABLE(ios(13.4)) +{ + if (SDL_CursorVisible()) { + return nil; + } else { + 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); + } + } + } +} + +- (void)pencilHovering:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0)) +{ + switch (recognizer.state) { + case UIGestureRecognizerStateBegan: + case UIGestureRecognizerStateChanged: + UIKit_HandlePenHover(self, recognizer); + break; + + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: + // we track touches elsewhere, so if a hover "ends" we'll deal with that there. + 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(SDL_PLATFORM_TVOS) + +- (SDL_TouchDeviceType)touchTypeForTouch:(UITouch *)touch +{ + if (touch.type == UITouchTypeIndirect) { + return SDL_TOUCH_DEVICE_INDIRECT_RELATIVE; + } + return SDL_TOUCH_DEVICE_DIRECT; +} + +- (SDL_TouchID)touchIdForType:(SDL_TouchDeviceType)type +{ + switch (type) { + case SDL_TOUCH_DEVICE_DIRECT: + default: + return directTouchId; + case SDL_TOUCH_DEVICE_INDIRECT_RELATIVE: + return indirectTouchId; + } +} + +- (CGPoint)touchLocation:(UITouch *)touch shouldNormalize:(BOOL)normalize +{ + CGPoint point = [touch locationInView:self]; + + if (normalize) { + CGRect bounds = self.bounds; + point.x /= bounds.size.width; + point.y /= bounds.size.height; + } + + return point; +} + +- (float)pressureForTouch:(UITouch *)touch +{ + return (float)touch.force; +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + for (UITouch *touch in touches) { +#if !defined(SDL_PLATFORM_TVOS) + if (@available(iOS 13.0, *)) { + if (touch.type == UITouchTypePencil) { + [self pencilPressed:touch]; + continue; + } + } + + if (@available(iOS 13.4, *)) { + if (touch.type == UITouchTypeIndirectPointer) { + [self indirectPointerPressed:touch fromEvent:event]; + continue; + } + } +#endif // !defined(SDL_PLATFORM_TVOS) + + SDL_TouchDeviceType touchType = [self touchTypeForTouch:touch]; + SDL_TouchID touchId = [self touchIdForType:touchType]; + float pressure = [self pressureForTouch:touch]; + + if (SDL_AddTouch(touchId, touchType, "") < 0) { + continue; + } + + // 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, + SDL_EVENT_FINGER_DOWN, locationInView.x, locationInView.y, pressure); + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + for (UITouch *touch in touches) { +#if !defined(SDL_PLATFORM_TVOS) + if (@available(iOS 13.0, *)) { + if (touch.type == UITouchTypePencil) { + [self pencilReleased:touch]; + continue; + } + } + + if (@available(iOS 13.4, *)) { + if (touch.type == UITouchTypeIndirectPointer) { + [self indirectPointerReleased:touch fromEvent:event]; + continue; + } + } +#endif // !defined(SDL_PLATFORM_TVOS) + + SDL_TouchDeviceType touchType = [self touchTypeForTouch:touch]; + SDL_TouchID touchId = [self touchIdForType:touchType]; + float pressure = [self pressureForTouch:touch]; + + if (SDL_AddTouch(touchId, touchType, "") < 0) { + continue; + } + + // 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, + SDL_EVENT_FINGER_UP, locationInView.x, locationInView.y, pressure); + } +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + for (UITouch *touch in touches) { +#if !defined(SDL_PLATFORM_TVOS) + if (@available(iOS 13.0, *)) { + if (touch.type == UITouchTypePencil) { + [self pencilReleased:touch]; + continue; + } + } + + if (@available(iOS 13.4, *)) { + if (touch.type == UITouchTypeIndirectPointer) { + [self indirectPointerReleased:touch fromEvent:event]; + continue; + } + } +#endif // !defined(SDL_PLATFORM_TVOS) + + 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_SendTouch(UIKit_GetEventTimestamp([event timestamp]), + touchId, (SDL_FingerID)(uintptr_t)touch, sdlwindow, + SDL_EVENT_FINGER_CANCELED, locationInView.x, locationInView.y, pressure); + } +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + for (UITouch *touch in touches) { +#if !defined(SDL_PLATFORM_TVOS) + if (@available(iOS 13.0, *)) { + if (touch.type == UITouchTypePencil) { + [self pencilMoving:touch]; + continue; + } + } + + if (@available(iOS 13.4, *)) { + if (touch.type == UITouchTypeIndirectPointer) { + [self indirectPointerMoving:touch]; + continue; + } + } +#endif // !defined(SDL_PLATFORM_TVOS) + + 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); + } +} + +- (void)safeAreaInsetsDidChange +{ + // Update the safe area insets + SDL_SetWindowSafeAreaInsets(sdlwindow, + (int)SDL_ceilf(self.safeAreaInsets.left), + (int)SDL_ceilf(self.safeAreaInsets.right), + (int)SDL_ceilf(self.safeAreaInsets.top), + (int)SDL_ceilf(self.safeAreaInsets.bottom)); +} + +- (SDL_Scancode)scancodeFromPress:(UIPress *)press +{ + if (press.key != nil) { + return (SDL_Scancode)press.key.keyCode; + } + +#ifndef SDL_JOYSTICK_DISABLED + // Presses from Apple TV remote + if (!SDL_AppleTVRemoteOpenedAsJoystick) { + switch (press.type) { + case UIPressTypeUpArrow: + return SDL_SCANCODE_UP; + case UIPressTypeDownArrow: + return SDL_SCANCODE_DOWN; + case UIPressTypeLeftArrow: + return SDL_SCANCODE_LEFT; + case UIPressTypeRightArrow: + return SDL_SCANCODE_RIGHT; + case UIPressTypeSelect: + // HIG says: "primary button behavior" + return SDL_SCANCODE_RETURN; + case UIPressTypeMenu: + // HIG says: "returns to previous screen" + return SDL_SCANCODE_ESCAPE; + case UIPressTypePlayPause: + // HIG says: "secondary button behavior" + return SDL_SCANCODE_PAUSE; + default: + break; + } + } +#endif // !SDL_JOYSTICK_DISABLED + + return SDL_SCANCODE_UNKNOWN; +} + +- (void)pressesBegan:(NSSet *)presses withEvent:(UIPressesEvent *)event +{ + if (!SDL_HasKeyboard()) { + for (UIPress *press in presses) { + SDL_Scancode scancode = [self scancodeFromPress:press]; + SDL_SendKeyboardKey(UIKit_GetEventTimestamp([event timestamp]), SDL_GLOBAL_KEYBOARD_ID, 0, scancode, true); + } + } + if (SDL_TextInputActive(sdlwindow)) { + [super pressesBegan:presses withEvent:event]; + } +} + +- (void)pressesEnded:(NSSet *)presses withEvent:(UIPressesEvent *)event +{ + if (!SDL_HasKeyboard()) { + for (UIPress *press in presses) { + SDL_Scancode scancode = [self scancodeFromPress:press]; + SDL_SendKeyboardKey(UIKit_GetEventTimestamp([event timestamp]), SDL_GLOBAL_KEYBOARD_ID, 0, scancode, false); + } + } + if (SDL_TextInputActive(sdlwindow)) { + [super pressesEnded:presses withEvent:event]; + } +} + +- (void)pressesCancelled:(NSSet *)presses withEvent:(UIPressesEvent *)event +{ + if (!SDL_HasKeyboard()) { + for (UIPress *press in presses) { + SDL_Scancode scancode = [self scancodeFromPress:press]; + SDL_SendKeyboardKey(UIKit_GetEventTimestamp([event timestamp]), SDL_GLOBAL_KEYBOARD_ID, 0, scancode, false); + } + } + if (SDL_TextInputActive(sdlwindow)) { + [super pressesCancelled:presses withEvent:event]; + } +} + +- (void)pressesChanged:(NSSet *)presses withEvent:(UIPressesEvent *)event +{ + // This is only called when the force of a press changes. + if (SDL_TextInputActive(sdlwindow)) { + [super pressesChanged:presses withEvent:event]; + } +} + +#ifdef SDL_PLATFORM_TVOS +- (void)swipeGesture:(UISwipeGestureRecognizer *)gesture +{ + // Swipe gestures don't trigger begin states. + if (gesture.state == UIGestureRecognizerStateEnded) { +#ifndef SDL_JOYSTICK_DISABLED + if (!SDL_AppleTVRemoteOpenedAsJoystick) { + /* Send arrow key presses for now, as we don't have an external API + * which better maps to swipe gestures. */ + switch (gesture.direction) { + case UISwipeGestureRecognizerDirectionUp: + SDL_SendKeyboardKeyAutoRelease(0, SDL_SCANCODE_UP); + break; + case UISwipeGestureRecognizerDirectionDown: + SDL_SendKeyboardKeyAutoRelease(0, SDL_SCANCODE_DOWN); + break; + case UISwipeGestureRecognizerDirectionLeft: + SDL_SendKeyboardKeyAutoRelease(0, SDL_SCANCODE_LEFT); + break; + case UISwipeGestureRecognizerDirectionRight: + SDL_SendKeyboardKeyAutoRelease(0, SDL_SCANCODE_RIGHT); + break; + } + } +#endif // !SDL_JOYSTICK_DISABLED + } +} +#endif // SDL_PLATFORM_TVOS + +@end + +#endif // SDL_VIDEO_DRIVER_UIKIT diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitviewcontroller.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitviewcontroller.h new file mode 100644 index 0000000..d52e478 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitviewcontroller.h @@ -0,0 +1,96 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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" + +#import + +#include "../SDL_sysvideo.h" + +#ifdef SDL_PLATFORM_TVOS +#import +#define SDLRootViewController GCEventViewController +#else +#define SDLRootViewController UIViewController +#endif + +@interface SDLUITextField : UITextField +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender; +@end + +#ifdef SDL_IPHONE_KEYBOARD +@interface SDL_uikitviewcontroller : SDLRootViewController +#else +@interface SDL_uikitviewcontroller : SDLRootViewController +#endif + +@property(nonatomic, assign) SDL_Window *window; + +- (instancetype)initWithSDLWindow:(SDL_Window *)_window; + +- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection; + +- (void)setAnimationCallback:(int)interval + callback:(void (*)(void *))callback + callbackParam:(void *)callbackParam; + +- (void)startAnimation; +- (void)stopAnimation; + +- (void)doLoop:(CADisplayLink *)sender; + +- (void)loadView; +- (void)viewDidLayoutSubviews; + +#ifndef SDL_PLATFORM_TVOS +- (NSUInteger)supportedInterfaceOrientations; +- (BOOL)prefersStatusBarHidden; +- (BOOL)prefersHomeIndicatorAutoHidden; +- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures; + +@property(nonatomic, assign) int homeIndicatorHidden; +#endif + +#ifdef SDL_IPHONE_KEYBOARD +- (bool)startTextInput; +- (bool)stopTextInput; +- (void)initKeyboard; +- (void)deinitKeyboard; + +- (void)keyboardWillShow:(NSNotification *)notification; +- (void)keyboardWillHide:(NSNotification *)notification; + +- (void)updateKeyboard; + +@property(nonatomic, assign, getter=isTextFieldFocused) BOOL textFieldFocused; +@property(nonatomic, assign) SDL_Rect textInputRect; +@property(nonatomic, assign) int keyboardHeight; +#endif + +@end + +#ifdef SDL_IPHONE_KEYBOARD +bool UIKit_HasScreenKeyboardSupport(SDL_VideoDevice *_this); +bool UIKit_StartTextInput(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props); +bool UIKit_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window); +void UIKit_SetTextInputProperties(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props); +bool UIKit_IsScreenKeyboardShown(SDL_VideoDevice *_this, SDL_Window *window); +bool UIKit_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window); +#endif diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitviewcontroller.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitviewcontroller.m new file mode 100644 index 0000000..45f2d64 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitviewcontroller.m @@ -0,0 +1,736 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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_sysvideo.h" +#include "../../events/SDL_events_c.h" + +#include "SDL_uikitviewcontroller.h" +#include "SDL_uikitmessagebox.h" +#include "SDL_uikitevents.h" +#include "SDL_uikitvideo.h" +#include "SDL_uikitmodes.h" +#include "SDL_uikitwindow.h" +#include "SDL_uikitopengles.h" + +#ifdef SDL_PLATFORM_TVOS +static void SDLCALL SDL_AppleTVControllerUIHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint) +{ + @autoreleasepool { + SDL_uikitviewcontroller *viewcontroller = (__bridge SDL_uikitviewcontroller *)userdata; + viewcontroller.controllerUserInteractionEnabled = hint && (*hint != '0'); + } +} +#endif + +#ifndef SDL_PLATFORM_TVOS +static void SDLCALL SDL_HideHomeIndicatorHintChanged(void *userdata, const char *name, const char *oldValue, const char *hint) +{ + @autoreleasepool { + SDL_uikitviewcontroller *viewcontroller = (__bridge SDL_uikitviewcontroller *)userdata; + viewcontroller.homeIndicatorHidden = (hint && *hint) ? SDL_atoi(hint) : -1; + [viewcontroller setNeedsUpdateOfHomeIndicatorAutoHidden]; + [viewcontroller setNeedsUpdateOfScreenEdgesDeferringSystemGestures]; + } +} +#endif + +@implementation SDLUITextField : UITextField +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + if (action == @selector(paste:)) { + return NO; + } + + return [super canPerformAction:action withSender:sender]; +} +@end + +@implementation SDL_uikitviewcontroller +{ + CADisplayLink *displayLink; + int animationInterval; + void (*animationCallback)(void *); + void *animationCallbackParam; + +#ifdef SDL_IPHONE_KEYBOARD + SDLUITextField *textField; + BOOL hidingKeyboard; + BOOL rotatingOrientation; + NSString *committedText; + NSString *obligateForBackspace; +#endif +} + +@synthesize window; + +- (instancetype)initWithSDLWindow:(SDL_Window *)_window +{ + if (self = [super initWithNibName:nil bundle:nil]) { + self.window = _window; + +#ifdef SDL_IPHONE_KEYBOARD + [self initKeyboard]; + hidingKeyboard = NO; + rotatingOrientation = NO; +#endif + +#ifdef SDL_PLATFORM_TVOS + SDL_AddHintCallback(SDL_HINT_APPLE_TV_CONTROLLER_UI_EVENTS, + SDL_AppleTVControllerUIHintChanged, + (__bridge void *)self); +#endif + +#ifndef SDL_PLATFORM_TVOS + SDL_AddHintCallback(SDL_HINT_IOS_HIDE_HOME_INDICATOR, + SDL_HideHomeIndicatorHintChanged, + (__bridge void *)self); +#endif + + // Enable high refresh rates on iOS + // To enable this on phones, you should add the following line to Info.plist: + // CADisableMinimumFrameDurationOnPhone + if (@available(iOS 15.0, tvOS 15.0, *)) { + const SDL_DisplayMode *mode = SDL_GetDesktopDisplayMode(SDL_GetPrimaryDisplay()); + if (mode && mode->refresh_rate > 60.0f) { + int frame_rate = (int)mode->refresh_rate; + displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(doLoop:)]; + displayLink.preferredFrameRateRange = CAFrameRateRangeMake((frame_rate * 2) / 3, frame_rate, frame_rate); + [displayLink addToRunLoop:NSRunLoop.currentRunLoop forMode:NSDefaultRunLoopMode]; + } + } + } + return self; +} + +- (void)dealloc +{ +#ifdef SDL_IPHONE_KEYBOARD + [self deinitKeyboard]; +#endif + +#ifdef SDL_PLATFORM_TVOS + SDL_RemoveHintCallback(SDL_HINT_APPLE_TV_CONTROLLER_UI_EVENTS, + SDL_AppleTVControllerUIHintChanged, + (__bridge void *)self); +#endif + +#ifndef SDL_PLATFORM_TVOS + SDL_RemoveHintCallback(SDL_HINT_IOS_HIDE_HOME_INDICATOR, + SDL_HideHomeIndicatorHintChanged, + (__bridge void *)self); +#endif +} + +- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection +{ + SDL_SetSystemTheme(UIKit_GetSystemTheme()); +} + +- (void)setAnimationCallback:(int)interval + callback:(void (*)(void *))callback + callbackParam:(void *)callbackParam +{ + [self stopAnimation]; + + if (interval <= 0) { + interval = 1; + } + animationInterval = interval; + animationCallback = callback; + animationCallbackParam = callbackParam; + + if (animationCallback) { + [self startAnimation]; + } +} + +- (void)startAnimation +{ + displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(doLoop:)]; + +#ifdef SDL_PLATFORM_VISIONOS + displayLink.preferredFramesPerSecond = 90 / animationInterval; //TODO: Get frame max frame rate on visionOS +#else + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + + displayLink.preferredFramesPerSecond = data.uiwindow.screen.maximumFramesPerSecond / animationInterval; +#endif + + [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; +} + +- (void)stopAnimation +{ + [displayLink invalidate]; + displayLink = nil; +} + +- (void)doLoop:(CADisplayLink *)sender +{ + // Don't run the game loop while a messagebox is up + if (animationCallback && !UIKit_ShowingMessageBox()) { + // See the comment in the function definition. +#if defined(SDL_VIDEO_OPENGL_ES) || defined(SDL_VIDEO_OPENGL_ES2) + UIKit_GL_RestoreCurrentContext(); +#endif + + animationCallback(animationCallbackParam); + } +} + +- (void)loadView +{ + // Do nothing. +} + +- (void)viewDidLayoutSubviews +{ + const CGSize size = self.view.bounds.size; + int w = (int)size.width; + int h = (int)size.height; + + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_RESIZED, w, h); +} + +#ifndef SDL_PLATFORM_TVOS +- (NSUInteger)supportedInterfaceOrientations +{ + return UIKit_GetSupportedOrientations(window); +} + +- (BOOL)prefersStatusBarHidden +{ + BOOL hidden = (window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS)) != 0; + return hidden; +} + +- (BOOL)prefersHomeIndicatorAutoHidden +{ + BOOL hidden = NO; + if (self.homeIndicatorHidden == 1) { + hidden = YES; + } + return hidden; +} + +- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures +{ + if (self.homeIndicatorHidden >= 0) { + if (self.homeIndicatorHidden == 2) { + return UIRectEdgeAll; + } else { + return UIRectEdgeNone; + } + } + + // By default, fullscreen and borderless windows get all screen gestures + if ((window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS)) != 0) { + return UIRectEdgeAll; + } else { + return UIRectEdgeNone; + } +} + +- (BOOL)prefersPointerLocked +{ + return SDL_GCMouseRelativeMode() ? YES : NO; +} + +#endif // !SDL_PLATFORM_TVOS + +/* + ---- Keyboard related functionality below this line ---- + */ +#ifdef SDL_IPHONE_KEYBOARD + +@synthesize textInputRect; +@synthesize keyboardHeight; +@synthesize textFieldFocused; + +// Set ourselves up as a UITextFieldDelegate +- (void)initKeyboard +{ + obligateForBackspace = @" "; // 64 space + textField = [[SDLUITextField alloc] initWithFrame:CGRectZero]; + textField.delegate = self; + // placeholder so there is something to delete! + textField.text = obligateForBackspace; + committedText = textField.text; + + textField.hidden = YES; + textFieldFocused = NO; + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; +#ifndef SDL_PLATFORM_TVOS + [center addObserver:self + selector:@selector(keyboardWillShow:) + name:UIKeyboardWillShowNotification + object:nil]; + [center addObserver:self + selector:@selector(keyboardWillHide:) + name:UIKeyboardWillHideNotification + object:nil]; + [center addObserver:self + selector:@selector(keyboardDidHide:) + name:UIKeyboardDidHideNotification + object:nil]; +#endif + [center addObserver:self + selector:@selector(textFieldTextDidChange:) + name:UITextFieldTextDidChangeNotification + object:nil]; +} + +- (NSArray *)keyCommands +{ + NSMutableArray *commands = [[NSMutableArray alloc] init]; + [commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow modifierFlags:kNilOptions action:@selector(handleCommand:)]]; + [commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow modifierFlags:kNilOptions action:@selector(handleCommand:)]]; + [commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow modifierFlags:kNilOptions action:@selector(handleCommand:)]]; + [commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow modifierFlags:kNilOptions action:@selector(handleCommand:)]]; + [commands addObject:[UIKeyCommand keyCommandWithInput:UIKeyInputEscape modifierFlags:kNilOptions action:@selector(handleCommand:)]]; + return [NSArray arrayWithArray:commands]; +} + +- (void)handleCommand:(UIKeyCommand *)keyCommand +{ + SDL_Scancode scancode = SDL_SCANCODE_UNKNOWN; + NSString *input = keyCommand.input; + + if (input == UIKeyInputUpArrow) { + scancode = SDL_SCANCODE_UP; + } else if (input == UIKeyInputDownArrow) { + scancode = SDL_SCANCODE_DOWN; + } else if (input == UIKeyInputLeftArrow) { + scancode = SDL_SCANCODE_LEFT; + } else if (input == UIKeyInputRightArrow) { + scancode = SDL_SCANCODE_RIGHT; + } else if (input == UIKeyInputEscape) { + scancode = SDL_SCANCODE_ESCAPE; + } + + if (scancode != SDL_SCANCODE_UNKNOWN) { + SDL_SendKeyboardKeyAutoRelease(0, scancode); + } +} + +- (void)setView:(UIView *)view +{ + [super setView:view]; + + [view addSubview:textField]; + + if (textFieldFocused) { + /* startTextInput has been called before the text field was added to the view, + * call it again for the text field to actually become first responder. */ + [self startTextInput]; + } +} + +- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator +{ + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + rotatingOrientation = YES; + [coordinator + animateAlongsideTransition:^(id context) { + } + completion:^(id context) { + self->rotatingOrientation = NO; + }]; +} + +- (void)deinitKeyboard +{ + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; +#ifndef SDL_PLATFORM_TVOS + [center removeObserver:self + name:UIKeyboardWillShowNotification + object:nil]; + [center removeObserver:self + name:UIKeyboardWillHideNotification + object:nil]; + [center removeObserver:self + name:UIKeyboardDidHideNotification + object:nil]; +#endif + [center removeObserver:self + name:UITextFieldTextDidChangeNotification + object:nil]; +} + +- (void)setTextFieldProperties:(SDL_PropertiesID) props +{ + textField.secureTextEntry = NO; + + switch (SDL_GetTextInputType(props)) { + default: + case SDL_TEXTINPUT_TYPE_TEXT: + textField.keyboardType = UIKeyboardTypeDefault; + textField.textContentType = nil; + break; + case SDL_TEXTINPUT_TYPE_TEXT_NAME: + textField.keyboardType = UIKeyboardTypeDefault; + textField.textContentType = UITextContentTypeName; + break; + case SDL_TEXTINPUT_TYPE_TEXT_EMAIL: + textField.keyboardType = UIKeyboardTypeEmailAddress; + textField.textContentType = UITextContentTypeEmailAddress; + break; + case SDL_TEXTINPUT_TYPE_TEXT_USERNAME: + textField.keyboardType = UIKeyboardTypeDefault; + textField.textContentType = UITextContentTypeUsername; + break; + case SDL_TEXTINPUT_TYPE_TEXT_PASSWORD_HIDDEN: + textField.keyboardType = UIKeyboardTypeDefault; + textField.textContentType = UITextContentTypePassword; + textField.secureTextEntry = YES; + break; + case SDL_TEXTINPUT_TYPE_TEXT_PASSWORD_VISIBLE: + textField.keyboardType = UIKeyboardTypeDefault; + textField.textContentType = UITextContentTypePassword; + break; + case SDL_TEXTINPUT_TYPE_NUMBER: + textField.keyboardType = UIKeyboardTypeDecimalPad; + textField.textContentType = nil; + break; + case SDL_TEXTINPUT_TYPE_NUMBER_PASSWORD_HIDDEN: + textField.keyboardType = UIKeyboardTypeNumberPad; + if (@available(iOS 12.0, tvOS 12.0, *)) { + textField.textContentType = UITextContentTypeOneTimeCode; + } else { + textField.textContentType = nil; + } + textField.secureTextEntry = YES; + break; + case SDL_TEXTINPUT_TYPE_NUMBER_PASSWORD_VISIBLE: + textField.keyboardType = UIKeyboardTypeNumberPad; + if (@available(iOS 12.0, tvOS 12.0, *)) { + textField.textContentType = UITextContentTypeOneTimeCode; + } else { + textField.textContentType = nil; + } + break; + } + + switch (SDL_GetTextInputCapitalization(props)) { + default: + case SDL_CAPITALIZE_NONE: + textField.autocapitalizationType = UITextAutocapitalizationTypeNone; + break; + case SDL_CAPITALIZE_LETTERS: + textField.autocapitalizationType = UITextAutocapitalizationTypeAllCharacters; + break; + case SDL_CAPITALIZE_WORDS: + textField.autocapitalizationType = UITextAutocapitalizationTypeWords; + break; + case SDL_CAPITALIZE_SENTENCES: + textField.autocapitalizationType = UITextAutocapitalizationTypeSentences; + break; + } + + if (SDL_GetTextInputAutocorrect(props)) { + textField.autocorrectionType = UITextAutocorrectionTypeYes; + textField.spellCheckingType = UITextSpellCheckingTypeYes; + } else { + textField.autocorrectionType = UITextAutocorrectionTypeNo; + textField.spellCheckingType = UITextSpellCheckingTypeNo; + } + + if (SDL_GetTextInputMultiline(props)) { + textField.enablesReturnKeyAutomatically = YES; + } else { + textField.enablesReturnKeyAutomatically = NO; + } + + if (!textField.window) { + /* textField has not been added to the view yet, + we don't have to do anything. */ + return; + } + + // the text field needs to be re-added to the view in order to update correctly. + UIView *superview = textField.superview; + [textField removeFromSuperview]; + [superview addSubview:textField]; + + if (SDL_TextInputActive(window)) { + [textField becomeFirstResponder]; + } +} + +/* requests the SDL text field to become focused and accept text input. + * also shows the onscreen virtual keyboard if no hardware keyboard is attached. */ +- (bool)startTextInput +{ + textFieldFocused = YES; + if (!textField.window) { + /* textField has not been added to the view yet, + * we will try again when that happens. */ + return true; + } + + return [textField becomeFirstResponder]; +} + +/* requests the SDL text field to lose focus and stop accepting text input. + * also hides the onscreen virtual keyboard if no hardware keyboard is attached. */ +- (bool)stopTextInput +{ + textFieldFocused = NO; + if (!textField.window) { + /* textField has not been added to the view yet, + * we will try again when that happens. */ + return true; + } + + [self resetTextState]; + return [textField resignFirstResponder]; +} + +- (void)keyboardWillShow:(NSNotification *)notification +{ +#ifndef SDL_PLATFORM_TVOS + CGRect kbrect = [[notification userInfo][UIKeyboardFrameEndUserInfoKey] CGRectValue]; + + /* The keyboard rect is in the coordinate space of the screen/window, but we + * want its height in the coordinate space of the view. */ + kbrect = [self.view convertRect:kbrect fromView:nil]; + + [self setKeyboardHeight:(int)kbrect.size.height]; +#endif + + /* A keyboard hide transition has been interrupted with a show (keyboardWillHide has been called but keyboardDidHide didn't). + * since text input was stopped by the hide, we have to start it again. */ + if (hidingKeyboard) { + SDL_StartTextInput(window); + hidingKeyboard = NO; + } +} + +- (void)keyboardWillHide:(NSNotification *)notification +{ + hidingKeyboard = YES; + [self setKeyboardHeight:0]; + + /* When the user dismisses the software keyboard by the "hide" button in the bottom right corner, + * we want to reflect that on SDL_TextInputActive by calling SDL_StopTextInput...on certain conditions */ + if (SDL_TextInputActive(window) + /* keyboardWillHide gets called when a hardware keyboard is attached, + * keep text input state active if hiding while there is a hardware keyboard. + * if the hardware keyboard gets detached, the software keyboard will appear anyway. */ + && !SDL_HasKeyboard() + /* When the device changes orientation, a sequence of hide and show transitions are triggered. + * keep text input state active in this case. */ + && !rotatingOrientation) { + SDL_StopTextInput(window); + } +} + +- (void)keyboardDidHide:(NSNotification *)notification +{ + hidingKeyboard = NO; +} + +- (void)textFieldTextDidChange:(NSNotification *)notification +{ + if (textField.markedTextRange == nil) { + NSUInteger compareLength = SDL_min(textField.text.length, committedText.length); + NSUInteger matchLength; + + // Backspace over characters that are no longer in the string + for (matchLength = 0; matchLength < compareLength; ++matchLength) { + if ([committedText characterAtIndex:matchLength] != [textField.text characterAtIndex:matchLength]) { + break; + } + } + if (matchLength < committedText.length) { + size_t deleteLength = SDL_utf8strlen([[committedText substringFromIndex:matchLength] UTF8String]); + while (deleteLength > 0) { + // Send distinct down and up events for each backspace action + SDL_SendKeyboardKey(0, SDL_GLOBAL_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, true); + SDL_SendKeyboardKey(0, SDL_GLOBAL_KEYBOARD_ID, 0, SDL_SCANCODE_BACKSPACE, false); + --deleteLength; + } + } + + if (matchLength < textField.text.length) { + NSString *pendingText = [textField.text substringFromIndex:matchLength]; + if (!SDL_HardwareKeyboardKeyPressed()) { + /* Go through all the characters in the string we've been sent and + * convert them to key presses */ + NSUInteger i; + for (i = 0; i < pendingText.length; i++) { + SDL_SendKeyboardUnicodeKey(0, [pendingText characterAtIndex:i]); + } + } + SDL_SendKeyboardText([pendingText UTF8String]); + } + committedText = textField.text; + } +} + +- (void)updateKeyboard +{ + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *) window->internal; + + CGAffineTransform t = self.view.transform; + CGPoint offset = CGPointMake(0.0, 0.0); +#ifdef SDL_PLATFORM_VISIONOS + CGRect frame = UIKit_ComputeViewFrame(window); +#else + CGRect frame = UIKit_ComputeViewFrame(window, data.uiwindow.screen); +#endif + + if (self.keyboardHeight && self.textInputRect.h) { + int rectbottom = (int)(self.textInputRect.y + self.textInputRect.h); + int keybottom = (int)(self.view.bounds.size.height - self.keyboardHeight); + if (keybottom < rectbottom) { + offset.y = keybottom - rectbottom; + } + } + + /* Apply this view's transform (except any translation) to the offset, in + * order to orient it correctly relative to the frame's coordinate space. */ + t.tx = 0.0; + t.ty = 0.0; + offset = CGPointApplyAffineTransform(offset, t); + + // Apply the updated offset to the view's frame. + frame.origin.x += offset.x; + frame.origin.y += offset.y; + + self.view.frame = frame; +} + +- (void)setKeyboardHeight:(int)height +{ + keyboardHeight = height; + [self updateKeyboard]; +} + +// UITextFieldDelegate method. Invoked when user types something. +- (BOOL)textField:(UITextField *)_textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string +{ + if (textField.markedTextRange == nil) { + if (textField.text.length < 16) { + [self resetTextState]; + } + } + return YES; +} + +// Terminates the editing session +- (BOOL)textFieldShouldReturn:(UITextField *)_textField +{ + SDL_SendKeyboardKeyAutoRelease(0, SDL_SCANCODE_RETURN); + if (textFieldFocused && + SDL_GetHintBoolean(SDL_HINT_RETURN_KEY_HIDES_IME, false)) { + SDL_StopTextInput(window); + } + return YES; +} + +- (void)resetTextState +{ + textField.text = obligateForBackspace; + committedText = textField.text; +} + +#endif + +@end + +// iPhone keyboard addition functions +#ifdef SDL_IPHONE_KEYBOARD + +static SDL_uikitviewcontroller *GetWindowViewController(SDL_Window *window) +{ + if (!window || !window->internal) { + SDL_SetError("Invalid window"); + return nil; + } + + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + + return data.viewcontroller; +} + +bool UIKit_HasScreenKeyboardSupport(SDL_VideoDevice *_this) +{ + return true; +} + +bool UIKit_StartTextInput(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props) +{ + @autoreleasepool { + SDL_uikitviewcontroller *vc = GetWindowViewController(window); + return [vc startTextInput]; + } +} + +bool UIKit_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_uikitviewcontroller *vc = GetWindowViewController(window); + return [vc stopTextInput]; + } +} + +void UIKit_SetTextInputProperties(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props) +{ + @autoreleasepool { + SDL_uikitviewcontroller *vc = GetWindowViewController(window); + [vc setTextFieldProperties:props]; + } +} + +bool UIKit_IsScreenKeyboardShown(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_uikitviewcontroller *vc = GetWindowViewController(window); + if (vc != nil) { + return vc.textFieldFocused; + } + return false; + } +} + +bool UIKit_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_uikitviewcontroller *vc = GetWindowViewController(window); + if (vc != nil) { + vc.textInputRect = window->text_input_rect; + + if (vc.textFieldFocused) { + [vc updateKeyboard]; + } + } + } + return true; +} + +#endif // SDL_IPHONE_KEYBOARD + +#endif // SDL_VIDEO_DRIVER_UIKIT diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvulkan.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvulkan.h new file mode 100644 index 0000000..6957670 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvulkan.h @@ -0,0 +1,52 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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. +*/ + +/* + * @author Mark Callow, www.edgewise-consulting.com. Based on Jacob Lifshay's + * SDL_x11vulkan.h. + */ + +#include "SDL_internal.h" + +#ifndef SDL_uikitvulkan_h_ +#define SDL_uikitvulkan_h_ + +#include "../SDL_vulkan_internal.h" +#include "../SDL_sysvideo.h" + +#if defined(SDL_VIDEO_VULKAN) && defined(SDL_VIDEO_DRIVER_UIKIT) + +extern bool UIKit_Vulkan_LoadLibrary(SDL_VideoDevice *_this, const char *path); +extern void UIKit_Vulkan_UnloadLibrary(SDL_VideoDevice *_this); +extern char const* const* UIKit_Vulkan_GetInstanceExtensions(SDL_VideoDevice *_this, Uint32 *count); +extern bool UIKit_Vulkan_CreateSurface(SDL_VideoDevice *_this, + SDL_Window *window, + VkInstance instance, + const struct VkAllocationCallbacks *allocator, + VkSurfaceKHR *surface); +extern void UIKit_Vulkan_DestroySurface(SDL_VideoDevice *_this, + VkInstance instance, + VkSurfaceKHR surface, + const struct VkAllocationCallbacks *allocator); + +#endif + +#endif // SDL_uikitvulkan_h_ diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvulkan.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvulkan.m new file mode 100644 index 0000000..332593b --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitvulkan.m @@ -0,0 +1,265 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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. +*/ + +/* + * @author Mark Callow, www.edgewise-consulting.com. Based on Jacob Lifshay's + * SDL_x11vulkan.c. + */ + +#include "SDL_internal.h" + +#if defined(SDL_VIDEO_VULKAN) && defined(SDL_VIDEO_DRIVER_UIKIT) + +#include "SDL_uikitvideo.h" +#include "SDL_uikitwindow.h" + +#include "SDL_uikitvulkan.h" +#include "SDL_uikitmetalview.h" + +#include + +const char *defaultPaths[] = { + "libvulkan.dylib", +}; + +/* Since libSDL is static, could use RTLD_SELF. Using RTLD_DEFAULT is future + * proofing. */ +#define DEFAULT_HANDLE RTLD_DEFAULT + +bool UIKit_Vulkan_LoadLibrary(SDL_VideoDevice *_this, const char *path) +{ + VkExtensionProperties *extensions = NULL; + Uint32 extensionCount = 0; + bool hasSurfaceExtension = false; + bool hasMetalSurfaceExtension = false; + bool hasIOSSurfaceExtension = false; + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = NULL; + + if (_this->vulkan_config.loader_handle) { + return SDL_SetError("Vulkan Portability library is already loaded."); + } + + // Load the Vulkan loader library + if (!path) { + path = SDL_GetHint(SDL_HINT_VULKAN_LIBRARY); + } + + if (!path) { + // Handle the case where Vulkan Portability is linked statically. + vkGetInstanceProcAddr = + (PFN_vkGetInstanceProcAddr)dlsym(DEFAULT_HANDLE, + "vkGetInstanceProcAddr"); + } + + if (vkGetInstanceProcAddr) { + _this->vulkan_config.loader_handle = DEFAULT_HANDLE; + } else { + const char **paths; + const char *foundPath = NULL; + int numPaths; + int i; + + if (path) { + paths = &path; + numPaths = 1; + } else { + // Look for the .dylib packaged with the application instead. + paths = defaultPaths; + numPaths = SDL_arraysize(defaultPaths); + } + + for (i = 0; i < numPaths && _this->vulkan_config.loader_handle == NULL; i++) { + foundPath = paths[i]; + _this->vulkan_config.loader_handle = SDL_LoadObject(foundPath); + } + + if (_this->vulkan_config.loader_handle == NULL) { + return SDL_SetError("Failed to load Vulkan Portability library"); + } + + SDL_strlcpy(_this->vulkan_config.loader_path, path, + SDL_arraysize(_this->vulkan_config.loader_path)); + vkGetInstanceProcAddr = + (PFN_vkGetInstanceProcAddr)SDL_LoadFunction( + _this->vulkan_config.loader_handle, + "vkGetInstanceProcAddr"); + } + + if (!vkGetInstanceProcAddr) { + SDL_SetError("Failed to find %s in either executable or %s: %s", + "vkGetInstanceProcAddr", + "linked Vulkan Portability library", + (const char *)dlerror()); + goto fail; + } + + _this->vulkan_config.vkGetInstanceProcAddr = (void *)vkGetInstanceProcAddr; + _this->vulkan_config.vkEnumerateInstanceExtensionProperties = + (void *)((PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr)( + VK_NULL_HANDLE, "vkEnumerateInstanceExtensionProperties"); + + if (!_this->vulkan_config.vkEnumerateInstanceExtensionProperties) { + SDL_SetError("No vkEnumerateInstanceExtensionProperties found."); + goto fail; + } + + extensions = SDL_Vulkan_CreateInstanceExtensionsList( + (PFN_vkEnumerateInstanceExtensionProperties) + _this->vulkan_config.vkEnumerateInstanceExtensionProperties, + &extensionCount); + + if (!extensions) { + goto fail; + } + + for (Uint32 i = 0; i < extensionCount; i++) { + if (SDL_strcmp(VK_KHR_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) { + hasSurfaceExtension = true; + } else if (SDL_strcmp(VK_EXT_METAL_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) { + hasMetalSurfaceExtension = true; + } else if (SDL_strcmp(VK_MVK_IOS_SURFACE_EXTENSION_NAME, extensions[i].extensionName) == 0) { + hasIOSSurfaceExtension = true; + } + } + + SDL_free(extensions); + + if (!hasSurfaceExtension) { + SDL_SetError("Installed Vulkan Portability doesn't implement the " VK_KHR_SURFACE_EXTENSION_NAME " extension"); + goto fail; + } else if (!hasMetalSurfaceExtension && !hasIOSSurfaceExtension) { + SDL_SetError("Installed Vulkan Portability doesn't implement the " VK_EXT_METAL_SURFACE_EXTENSION_NAME " or " VK_MVK_IOS_SURFACE_EXTENSION_NAME " extensions"); + goto fail; + } + + return true; + +fail: + _this->vulkan_config.loader_handle = NULL; + return false; +} + +void UIKit_Vulkan_UnloadLibrary(SDL_VideoDevice *_this) +{ + if (_this->vulkan_config.loader_handle) { + if (_this->vulkan_config.loader_handle != DEFAULT_HANDLE) { + SDL_UnloadObject(_this->vulkan_config.loader_handle); + } + _this->vulkan_config.loader_handle = NULL; + } +} + +char const* const* UIKit_Vulkan_GetInstanceExtensions(SDL_VideoDevice *_this, + Uint32 *count) +{ + static const char *const extensionsForUIKit[] = { + VK_KHR_SURFACE_EXTENSION_NAME, VK_EXT_METAL_SURFACE_EXTENSION_NAME + }; + if(count) { + *count = SDL_arraysize(extensionsForUIKit); + } + return extensionsForUIKit; +} + +bool UIKit_Vulkan_CreateSurface(SDL_VideoDevice *_this, + SDL_Window *window, + VkInstance instance, + const struct VkAllocationCallbacks *allocator, + VkSurfaceKHR *surface) +{ + PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = + (PFN_vkGetInstanceProcAddr)_this->vulkan_config.vkGetInstanceProcAddr; + PFN_vkCreateMetalSurfaceEXT vkCreateMetalSurfaceEXT = + (PFN_vkCreateMetalSurfaceEXT)vkGetInstanceProcAddr( + (VkInstance)instance, + "vkCreateMetalSurfaceEXT"); + PFN_vkCreateIOSSurfaceMVK vkCreateIOSSurfaceMVK = + (PFN_vkCreateIOSSurfaceMVK)vkGetInstanceProcAddr( + (VkInstance)instance, + "vkCreateIOSSurfaceMVK"); + VkResult result; + SDL_MetalView metalview; + + if (!_this->vulkan_config.loader_handle) { + return SDL_SetError("Vulkan is not loaded"); + } + + if (!vkCreateMetalSurfaceEXT && !vkCreateIOSSurfaceMVK) { + return SDL_SetError(VK_EXT_METAL_SURFACE_EXTENSION_NAME " or " VK_MVK_IOS_SURFACE_EXTENSION_NAME + " extensions are not enabled in the Vulkan instance."); + } + + metalview = UIKit_Metal_CreateView(_this, window); + if (metalview == NULL) { + return false; + } + + if (vkCreateMetalSurfaceEXT) { + VkMetalSurfaceCreateInfoEXT createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT; + createInfo.pNext = NULL; + createInfo.flags = 0; + createInfo.pLayer = (__bridge const CAMetalLayer *) + UIKit_Metal_GetLayer(_this, metalview); + result = vkCreateMetalSurfaceEXT(instance, &createInfo, allocator, surface); + if (result != VK_SUCCESS) { + UIKit_Metal_DestroyView(_this, metalview); + return SDL_SetError("vkCreateMetalSurfaceEXT failed: %s", SDL_Vulkan_GetResultString(result)); + } + } else { + VkIOSSurfaceCreateInfoMVK createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_IOS_SURFACE_CREATE_INFO_MVK; + createInfo.pNext = NULL; + createInfo.flags = 0; + createInfo.pView = (const void *)metalview; + result = vkCreateIOSSurfaceMVK(instance, &createInfo, + allocator, surface); + if (result != VK_SUCCESS) { + UIKit_Metal_DestroyView(_this, metalview); + return SDL_SetError("vkCreateIOSSurfaceMVK failed: %s", SDL_Vulkan_GetResultString(result)); + } + } + + /* Unfortunately there's no SDL_Vulkan_DestroySurface function we can call + * Metal_DestroyView from. Right now the metal view's ref count is +2 (one + * from returning a new view object in CreateView, and one because it's + * a subview of the window.) If we release the view here to make it +1, it + * will be destroyed when the window is destroyed. + * + * TODO: Now that we have SDL_Vulkan_DestroySurface someone with enough + * knowledge of Metal can proceed. */ + CFBridgingRelease(metalview); + + return true; +} + +void UIKit_Vulkan_DestroySurface(SDL_VideoDevice *_this, + VkInstance instance, + VkSurfaceKHR surface, + const struct VkAllocationCallbacks *allocator) +{ + if (_this->vulkan_config.loader_handle) { + SDL_Vulkan_DestroySurface_Internal(_this->vulkan_config.vkGetInstanceProcAddr, instance, surface, allocator); + // TODO: Add CFBridgingRelease(metalview) here perhaps? + } +} + +#endif diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitwindow.h b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitwindow.h new file mode 100644 index 0000000..da1fb63 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitwindow.h @@ -0,0 +1,56 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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_uikitwindow_h_ +#define SDL_uikitwindow_h_ + +#include "../SDL_sysvideo.h" +#import "SDL_uikitvideo.h" +#import "SDL_uikitview.h" +#import "SDL_uikitviewcontroller.h" + +extern bool UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props); +extern void UIKit_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window); +extern void UIKit_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern void UIKit_HideWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern void UIKit_RaiseWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern void UIKit_SetWindowBordered(SDL_VideoDevice *_this, SDL_Window *window, bool bordered); +extern SDL_FullscreenResult UIKit_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen); +extern void UIKit_UpdatePointerLock(SDL_VideoDevice *_this, SDL_Window *window); +extern void UIKit_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window); +extern void UIKit_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h); + +extern NSUInteger UIKit_GetSupportedOrientations(SDL_Window *window); + +#define SDL_METALVIEW_TAG 255 + +@class UIWindow; + +@interface SDL_UIKitWindowData : NSObject + +@property(nonatomic, strong) UIWindow *uiwindow; +@property(nonatomic, strong) SDL_uikitviewcontroller *viewcontroller; + +// Array of SDL_uikitviews owned by this window. +@property(nonatomic, copy) NSMutableArray *views; + +@end + +#endif // SDL_uikitwindow_h_ diff --git a/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitwindow.m b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitwindow.m new file mode 100644 index 0000000..2b258b1 --- /dev/null +++ b/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitwindow.m @@ -0,0 +1,471 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2025 Sam Lantinga + + 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_sysvideo.h" +#include "../SDL_pixels_c.h" +#include "../../events/SDL_events_c.h" + +#include "SDL_uikitvideo.h" +#include "SDL_uikitevents.h" +#include "SDL_uikitmodes.h" +#include "SDL_uikitwindow.h" +#include "SDL_uikitappdelegate.h" +#include "SDL_uikitview.h" +#include "SDL_uikitopenglview.h" + +#include + +@implementation SDL_UIKitWindowData + +@synthesize uiwindow; +@synthesize viewcontroller; +@synthesize views; + +- (instancetype)init +{ + if ((self = [super init])) { + views = [NSMutableArray new]; + } + + return self; +} + +@end + +@interface SDL_uikitwindow : UIWindow + +- (void)layoutSubviews; + +@end + +@implementation SDL_uikitwindow + +- (void)layoutSubviews +{ +#ifndef SDL_PLATFORM_VISIONOS + // Workaround to fix window orientation issues in iOS 8. + /* As of July 1 2019, I haven't been able to reproduce any orientation + * issues with this disabled on iOS 12. The issue this is meant to fix might + * only happen on iOS 8, or it might have been fixed another way with other + * code... This code prevents split view (iOS 9+) from working on iPads, so + * we want to avoid using it if possible. */ + if (!UIKit_IsSystemVersionAtLeast(9.0)) { + self.frame = self.screen.bounds; + } +#endif + [super layoutSubviews]; +} + +@end + +static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow *uiwindow, bool created) +{ + SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window); + SDL_UIKitDisplayData *displaydata = (__bridge SDL_UIKitDisplayData *)display->internal; + SDL_uikitview *view; + +#ifdef SDL_PLATFORM_VISIONOS + CGRect frame = UIKit_ComputeViewFrame(window); +#else + CGRect frame = UIKit_ComputeViewFrame(window, displaydata.uiscreen); +#endif + + int width = (int)frame.size.width; + int height = (int)frame.size.height; + + SDL_UIKitWindowData *data = [[SDL_UIKitWindowData alloc] init]; + if (!data) { + return SDL_OutOfMemory(); + } + + window->internal = (SDL_WindowData *)CFBridgingRetain(data); + + data.uiwindow = uiwindow; + +#ifndef SDL_PLATFORM_VISIONOS + if (displaydata.uiscreen != [UIScreen mainScreen]) { + window->flags &= ~SDL_WINDOW_RESIZABLE; // window is NEVER resizable + window->flags &= ~SDL_WINDOW_INPUT_FOCUS; // never has input focus + window->flags |= SDL_WINDOW_BORDERLESS; // never has a status bar. + } +#endif + +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) + if (displaydata.uiscreen == [UIScreen mainScreen]) { + NSUInteger orients = UIKit_GetSupportedOrientations(window); + BOOL supportsLandscape = (orients & UIInterfaceOrientationMaskLandscape) != 0; + BOOL supportsPortrait = (orients & (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown)) != 0; + + // Make sure the width/height are oriented correctly + if ((width > height && !supportsLandscape) || (height > width && !supportsPortrait)) { + int temp = width; + width = height; + height = temp; + } + } +#endif // !SDL_PLATFORM_TVOS + +#if 0 // Don't set the x/y position, it's already placed on a display + window->x = 0; + window->y = 0; +#endif + window->w = width; + window->h = height; + + /* The View Controller will handle rotating the view when the device + * orientation changes. This will trigger resize events, if appropriate. */ + data.viewcontroller = [[SDL_uikitviewcontroller alloc] initWithSDLWindow:window]; + + /* The window will initially contain a generic view so resizes, touch events, + * etc. can be handled without an active OpenGL view/context. */ + view = [[SDL_uikitview alloc] initWithFrame:frame]; + + /* Sets this view as the controller's view, and adds the view to the window + * hierarchy. */ + [view setSDLWindow:window]; + + SDL_PropertiesID props = SDL_GetWindowProperties(window); + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, (__bridge void *)data.uiwindow); + SDL_SetNumberProperty(props, SDL_PROP_WINDOW_UIKIT_METAL_VIEW_TAG_NUMBER, SDL_METALVIEW_TAG); + + return true; +} + +bool UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props) +{ + @autoreleasepool { + SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window); + SDL_UIKitDisplayData *data = (__bridge SDL_UIKitDisplayData *)display->internal; + SDL_Window *other; + + // We currently only handle a single window per display on iOS + for (other = _this->windows; other; other = other->next) { + if (other != window && SDL_GetVideoDisplayForWindow(other) == display) { + return SDL_SetError("Only one window allowed per display."); + } + } + + /* If monitor has a resolution of 0x0 (hasn't been explicitly set by the + * user, so it's in standby), try to force the display to a resolution + * that most closely matches the desired window size. */ +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) + const CGSize origsize = data.uiscreen.currentMode.size; + if ((origsize.width == 0.0f) && (origsize.height == 0.0f)) { + SDL_DisplayMode bestmode; + bool include_high_density_modes = false; + if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) { + include_high_density_modes = true; + } + if (SDL_GetClosestFullscreenDisplayMode(display->id, window->w, window->h, 0.0f, include_high_density_modes, &bestmode)) { + SDL_UIKitDisplayModeData *modedata = (__bridge SDL_UIKitDisplayModeData *)bestmode.internal; + [data.uiscreen setCurrentMode:modedata.uiscreenmode]; + + /* desktop_mode doesn't change here (the higher level will + * use it to set all the screens back to their defaults + * upon window destruction, SDL_Quit(), etc. */ + SDL_SetCurrentDisplayMode(display, &bestmode); + } + } + + if (data.uiscreen == [UIScreen mainScreen]) { + if (window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS)) { + [UIApplication sharedApplication].statusBarHidden = YES; + } else { + [UIApplication sharedApplication].statusBarHidden = NO; + } + } +#endif // !SDL_PLATFORM_TVOS + + // ignore the size user requested, and make a fullscreen window + // !!! FIXME: can we have a smaller view? +#ifdef SDL_PLATFORM_VISIONOS + UIWindow *uiwindow = [[SDL_uikitwindow alloc] initWithFrame:CGRectMake(window->x, window->y, window->w, window->h)]; +#else + UIWindow *uiwindow = [[SDL_uikitwindow alloc] initWithFrame:data.uiscreen.bounds]; +#endif + + // put the window on an external display if appropriate. +#ifndef SDL_PLATFORM_VISIONOS + if (data.uiscreen != [UIScreen mainScreen]) { + [uiwindow setScreen:data.uiscreen]; + } +#endif + + if (!SetupWindowData(_this, window, uiwindow, true)) { + return false; + } + } + + return true; +} + +void UIKit_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + data.viewcontroller.title = @(window->title); + } +} + +void UIKit_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + [data.uiwindow makeKeyAndVisible]; + + // Make this window the current mouse focus for touch input + SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window); + SDL_UIKitDisplayData *displaydata = (__bridge SDL_UIKitDisplayData *)display->internal; +#ifndef SDL_PLATFORM_VISIONOS + if (displaydata.uiscreen == [UIScreen mainScreen]) +#endif + { + SDL_SetMouseFocus(window); + SDL_SetKeyboardFocus(window); + } + } +} + +void UIKit_HideWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + data.uiwindow.hidden = YES; + } +} + +void UIKit_RaiseWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ +#if defined(SDL_VIDEO_OPENGL_ES) || defined(SDL_VIDEO_OPENGL_ES2) + /* We don't currently offer a concept of "raising" the SDL window, since + * we only allow one per display, in the iOS fashion. + * However, we use this entry point to rebind the context to the view + * during OnWindowRestored processing. */ + _this->GL_MakeCurrent(_this, _this->current_glwin, _this->current_glctx); +#endif +} + +static void UIKit_UpdateWindowBorder(SDL_VideoDevice *_this, SDL_Window *window) +{ + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + SDL_uikitviewcontroller *viewcontroller = data.viewcontroller; + +#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) + if (data.uiwindow.screen == [UIScreen mainScreen]) { + if (window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS)) { + [UIApplication sharedApplication].statusBarHidden = YES; + } else { + [UIApplication sharedApplication].statusBarHidden = NO; + } + + [viewcontroller setNeedsStatusBarAppearanceUpdate]; + } + + // Update the view's frame to account for the status bar change. + viewcontroller.view.frame = UIKit_ComputeViewFrame(window, data.uiwindow.screen); +#endif // !SDL_PLATFORM_TVOS + +#ifdef SDL_IPHONE_KEYBOARD + // Make sure the view is offset correctly when the keyboard is visible. + [viewcontroller updateKeyboard]; +#endif + + [viewcontroller.view setNeedsLayout]; + [viewcontroller.view layoutIfNeeded]; +} + +void UIKit_SetWindowBordered(SDL_VideoDevice *_this, SDL_Window *window, bool bordered) +{ + @autoreleasepool { + if (bordered) { + window->flags &= ~SDL_WINDOW_BORDERLESS; + } else { + window->flags |= SDL_WINDOW_BORDERLESS; + } + UIKit_UpdateWindowBorder(_this, window); + } +} + +SDL_FullscreenResult UIKit_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen) +{ + @autoreleasepool { + SDL_SendWindowEvent(window, fullscreen ? SDL_EVENT_WINDOW_ENTER_FULLSCREEN : SDL_EVENT_WINDOW_LEAVE_FULLSCREEN, 0, 0); + UIKit_UpdateWindowBorder(_this, window); + } + return SDL_FULLSCREEN_SUCCEEDED; +} + +void UIKit_UpdatePointerLock(SDL_VideoDevice *_this, SDL_Window *window) +{ +#ifndef SDL_PLATFORM_TVOS + @autoreleasepool { + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + SDL_uikitviewcontroller *viewcontroller = data.viewcontroller; + if (@available(iOS 14.0, *)) { + [viewcontroller setNeedsUpdateOfPrefersPointerLocked]; + } + } +#endif // !SDL_PLATFORM_TVOS +} + +void UIKit_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window) +{ + @autoreleasepool { + if (window->internal != NULL) { + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + NSArray *views = nil; + + [data.viewcontroller stopAnimation]; + + /* Detach all views from this window. We use a copy of the array + * because setSDLWindow will remove the object from the original + * array, which would be undesirable if we were iterating over it. */ + views = [data.views copy]; + for (SDL_uikitview *view in views) { + [view setSDLWindow:NULL]; + } + + /* iOS may still hold a reference to the window after we release it. + * We want to make sure the SDL view controller isn't accessed in + * that case, because it would contain an invalid pointer to the old + * SDL window. */ + data.uiwindow.rootViewController = nil; + data.uiwindow.hidden = YES; + + CFRelease(window->internal); + window->internal = NULL; + } + } +} + +void UIKit_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h) +{ + @autoreleasepool { + SDL_UIKitWindowData *windata = (__bridge SDL_UIKitWindowData *)window->internal; + UIView *view = windata.viewcontroller.view; + CGSize size = view.bounds.size; + CGFloat scale = 1.0; + + + if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) { +#ifndef SDL_PLATFORM_VISIONOS + scale = windata.uiwindow.screen.nativeScale; +#else + scale = 2.0; +#endif + } + + + /* Integer truncation of fractional values matches SDL_uikitmetalview and + * SDL_uikitopenglview. */ + *w = (int)(size.width * scale); + *h = (int)(size.height * scale); + } +} + +#ifndef SDL_PLATFORM_TVOS +NSUInteger +UIKit_GetSupportedOrientations(SDL_Window *window) +{ + const char *hint = SDL_GetHint(SDL_HINT_ORIENTATIONS); + NSUInteger validOrientations = UIInterfaceOrientationMaskAll; + NSUInteger orientationMask = 0; + + @autoreleasepool { + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + UIApplication *app = [UIApplication sharedApplication]; + + /* Get all possible valid orientations. If the app delegate doesn't tell + * us, we get the orientations from Info.plist via UIApplication. */ + if ([app.delegate respondsToSelector:@selector(application:supportedInterfaceOrientationsForWindow:)]) { + validOrientations = [app.delegate application:app supportedInterfaceOrientationsForWindow:data.uiwindow]; + } else { + validOrientations = [app supportedInterfaceOrientationsForWindow:data.uiwindow]; + } + + if (hint != NULL) { + NSArray *orientations = [@(hint) componentsSeparatedByString:@" "]; + + if ([orientations containsObject:@"LandscapeLeft"]) { + orientationMask |= UIInterfaceOrientationMaskLandscapeLeft; + } + if ([orientations containsObject:@"LandscapeRight"]) { + orientationMask |= UIInterfaceOrientationMaskLandscapeRight; + } + if ([orientations containsObject:@"Portrait"]) { + orientationMask |= UIInterfaceOrientationMaskPortrait; + } + if ([orientations containsObject:@"PortraitUpsideDown"]) { + orientationMask |= UIInterfaceOrientationMaskPortraitUpsideDown; + } + } + + if (orientationMask == 0 && (window->flags & SDL_WINDOW_RESIZABLE)) { + // any orientation is okay. + orientationMask = UIInterfaceOrientationMaskAll; + } + + if (orientationMask == 0) { + if (window->floating.w >= window->floating.h) { + orientationMask |= UIInterfaceOrientationMaskLandscape; + } + if (window->floating.h >= window->floating.w) { + orientationMask |= (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown); + } + } + + // Don't allow upside-down orientation on phones, so answering calls is in the natural orientation + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) { + orientationMask &= ~UIInterfaceOrientationMaskPortraitUpsideDown; + } + + /* If none of the specified orientations are actually supported by the + * app, we'll revert to what the app supports. An exception would be + * thrown by the system otherwise. */ + if ((validOrientations & orientationMask) == 0) { + orientationMask = validOrientations; + } + } + + return orientationMask; +} +#endif // !SDL_PLATFORM_TVOS + +bool SDL_SetiOSAnimationCallback(SDL_Window *window, int interval, SDL_iOSAnimationCallback callback, void *callbackParam) +{ + if (!window || !window->internal) { + return SDL_SetError("Invalid window"); + } + + @autoreleasepool { + SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; + [data.viewcontroller setAnimationCallback:interval + callback:callback + callbackParam:callbackParam]; + } + + return true; +} + +#endif // SDL_VIDEO_DRIVER_UIKIT -- cgit v1.2.3