Atlas - SDL_uikitwindow.m

Home / ext / SDL / src / video / uikit Lines: 1 | Size: 19550 bytes [Download] [Show on GitHub] [Search similar files] [Raw] [Raw (proxy)]
[FILE BEGIN]
1/* 2 Simple DirectMedia Layer 3 Copyright (C) 1997-2026 Sam Lantinga <[email protected]> 4 5 This software is provided 'as-is', without any express or implied 6 warranty. In no event will the authors be held liable for any damages 7 arising from the use of this software. 8 9 Permission is granted to anyone to use this software for any purpose, 10 including commercial applications, and to alter it and redistribute it 11 freely, subject to the following restrictions: 12 13 1. The origin of this software must not be misrepresented; you must not 14 claim that you wrote the original software. If you use this software 15 in a product, an acknowledgment in the product documentation would be 16 appreciated but is not required. 17 2. Altered source versions must be plainly marked as such, and must not be 18 misrepresented as being the original software. 19 3. This notice may not be removed or altered from any source distribution. 20*/ 21#include "SDL_internal.h" 22 23#ifdef SDL_VIDEO_DRIVER_UIKIT 24 25#include "../SDL_sysvideo.h" 26#include "../SDL_pixels_c.h" 27#include "../../events/SDL_events_c.h" 28 29#include "SDL_uikitvideo.h" 30#include "SDL_uikitevents.h" 31#include "SDL_uikitmodes.h" 32#include "SDL_uikitwindow.h" 33#include "SDL_uikitappdelegate.h" 34#include "SDL_uikitview.h" 35#include "SDL_uikitopenglview.h" 36 37#include <Foundation/Foundation.h> 38 39@implementation SDL_UIKitWindowData 40 41@synthesize uiwindow; 42@synthesize viewcontroller; 43@synthesize views; 44 45- (instancetype)init 46{ 47 if ((self = [super init])) { 48 views = [NSMutableArray new]; 49 } 50 51 return self; 52} 53 54@end 55 56static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow *uiwindow, bool created) 57{ 58 SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window); 59 SDL_UIKitDisplayData *displaydata = (__bridge SDL_UIKitDisplayData *)display->internal; 60 SDL_uikitview *view; 61 62#ifdef SDL_PLATFORM_VISIONOS 63 CGRect frame = UIKit_ComputeViewFrame(window); 64#else 65 CGRect frame = UIKit_ComputeViewFrame(window, displaydata.uiscreen); 66#endif 67 68 int width = (int)frame.size.width; 69 int height = (int)frame.size.height; 70 71 SDL_UIKitWindowData *data = [[SDL_UIKitWindowData alloc] init]; 72 if (!data) { 73 return SDL_OutOfMemory(); 74 } 75 76 window->internal = (SDL_WindowData *)CFBridgingRetain(data); 77 78 data.uiwindow = uiwindow; 79 80#ifndef SDL_PLATFORM_VISIONOS 81 if (displaydata.uiscreen != [UIScreen mainScreen]) { 82 window->flags &= ~SDL_WINDOW_RESIZABLE; // window is NEVER resizable 83 window->flags &= ~SDL_WINDOW_INPUT_FOCUS; // never has input focus 84 window->flags |= SDL_WINDOW_BORDERLESS; // never has a status bar. 85 } 86#endif 87 88#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) 89 if (displaydata.uiscreen == [UIScreen mainScreen]) { 90 NSUInteger orients = UIKit_GetSupportedOrientations(window); 91 BOOL supportsLandscape = (orients & UIInterfaceOrientationMaskLandscape) != 0; 92 BOOL supportsPortrait = (orients & (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown)) != 0; 93 94 // Make sure the width/height are oriented correctly 95 if ((width > height && !supportsLandscape) || (height > width && !supportsPortrait)) { 96 int temp = width; 97 width = height; 98 height = temp; 99 } 100 } 101#endif // !SDL_PLATFORM_TVOS 102 103#if 0 // Don't set the x/y position, it's already placed on a display 104 window->x = 0; 105 window->y = 0; 106#endif 107 window->w = width; 108 window->h = height; 109 110 /* The View Controller will handle rotating the view when the device 111 * orientation changes. This will trigger resize events, if appropriate. */ 112 data.viewcontroller = [[SDL_uikitviewcontroller alloc] initWithSDLWindow:window]; 113 114 /* The window will initially contain a generic view so resizes, touch events, 115 * etc. can be handled without an active OpenGL view/context. */ 116 view = [[SDL_uikitview alloc] initWithFrame:frame]; 117 118 /* Sets this view as the controller's view, and adds the view to the window 119 * hierarchy. */ 120 [view setSDLWindow:window]; 121 122 SDL_PropertiesID props = SDL_GetWindowProperties(window); 123 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, (__bridge void *)data.uiwindow); 124 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_UIKIT_METAL_VIEW_TAG_NUMBER, SDL_METALVIEW_TAG); 125 126 return true; 127} 128 129bool UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props) 130{ 131 @autoreleasepool { 132 SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window); 133 SDL_UIKitDisplayData *data = (__bridge SDL_UIKitDisplayData *)display->internal; 134 SDL_Window *other; 135 136 // We currently only handle a single window per display on iOS 137 for (other = _this->windows; other; other = other->next) { 138 if (other != window && SDL_GetVideoDisplayForWindow(other) == display) { 139 return SDL_SetError("Only one window allowed per display."); 140 } 141 } 142 143 /* If monitor has a resolution of 0x0 (hasn't been explicitly set by the 144 * user, so it's in standby), try to force the display to a resolution 145 * that most closely matches the desired window size. */ 146#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) 147 const CGSize origsize = data.uiscreen.currentMode.size; 148 if ((origsize.width == 0.0f) && (origsize.height == 0.0f)) { 149 SDL_DisplayMode bestmode; 150 bool include_high_density_modes = false; 151 if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) { 152 include_high_density_modes = true; 153 } 154 if (SDL_GetClosestFullscreenDisplayMode(display->id, window->w, window->h, 0.0f, include_high_density_modes, &bestmode)) { 155 SDL_UIKitDisplayModeData *modedata = (__bridge SDL_UIKitDisplayModeData *)bestmode.internal; 156 [data.uiscreen setCurrentMode:modedata.uiscreenmode]; 157 158 /* desktop_mode doesn't change here (the higher level will 159 * use it to set all the screens back to their defaults 160 * upon window destruction, SDL_Quit(), etc. */ 161 SDL_SetCurrentDisplayMode(display, &bestmode); 162 } 163 } 164 165 if (data.uiscreen == [UIScreen mainScreen]) { 166 if (@available(iOS 13.0, *)) { 167 // iOS 13+ uses view controller's prefersStatusBarHidden 168 } else { 169#pragma clang diagnostic push 170#pragma clang diagnostic ignored "-Wdeprecated-declarations" 171 if (window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS)) { 172 [UIApplication sharedApplication].statusBarHidden = YES; 173 } else { 174 [UIApplication sharedApplication].statusBarHidden = NO; 175 } 176#pragma clang diagnostic pop 177 } 178 } 179#endif // !SDL_PLATFORM_TVOS 180 181 UIWindow *uiwindow = nil; 182 if (@available(iOS 13.0, tvOS 13.0, *)) { 183 UIWindowScene *scene = (__bridge UIWindowScene *)SDL_GetPointerProperty(create_props, SDL_PROP_WINDOW_CREATE_WINDOWSCENE_POINTER, NULL); 184 if (!scene) { 185 scene = UIKit_GetActiveWindowScene(); 186 } 187 if (scene) { 188 uiwindow = [[UIWindow alloc] initWithWindowScene:scene]; 189 190#ifdef SDL_PLATFORM_VISIONOS 191 /* On visionOS, the window scene may not have its final geometry yet 192 * when the UIWindow is first created. Request the desired size now 193 * and set the UIWindow frame to match so views have valid initial 194 * dimensions before the async geometry update completes. */ 195 CGSize desiredSize = CGSizeMake(window->w, window->h); 196 uiwindow.frame = CGRectMake(0, 0, desiredSize.width, desiredSize.height); 197 198 UIWindowSceneGeometryPreferences *preferences = 199 [[UIWindowSceneGeometryPreferencesVision alloc] initWithSize:desiredSize]; 200 [scene requestGeometryUpdateWithPreferences:preferences errorHandler:^(NSError * _Nonnull error) { 201 SDL_LogWarn(SDL_LOG_CATEGORY_VIDEO, 202 "Initial geometry request failed: %s", 203 [[error localizedDescription] UTF8String]); 204 }]; 205#endif 206 } 207 } 208 if (!uiwindow) { 209 // ignore the size user requested, and make a fullscreen window 210#ifdef SDL_PLATFORM_VISIONOS 211 uiwindow = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, SDL_XR_SCREENWIDTH, SDL_XR_SCREENHEIGHT)]; 212#else 213 uiwindow = [[UIWindow alloc] initWithFrame:data.uiscreen.bounds]; 214#endif 215 } 216 217 // put the window on an external display if appropriate. 218#ifndef SDL_PLATFORM_VISIONOS 219 if (data.uiscreen != [UIScreen mainScreen]) { 220 if (@available(iOS 13.0, tvOS 13.0, *)) { 221 // iOS 13+ uses UIWindowScene to manage screen association 222 } else { 223#pragma clang diagnostic push 224#pragma clang diagnostic ignored "-Wdeprecated-declarations" 225 [uiwindow setScreen:data.uiscreen]; 226#pragma clang diagnostic pop 227 } 228 } 229#endif 230 231 if (!SetupWindowData(_this, window, uiwindow, true)) { 232 return false; 233 } 234 } 235 236 return true; 237} 238 239void UIKit_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window) 240{ 241 @autoreleasepool { 242 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; 243 data.viewcontroller.title = @(window->title); 244 } 245} 246 247void UIKit_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window) 248{ 249#ifdef SDL_PLATFORM_VISIONOS 250 @autoreleasepool { 251 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; 252 UIWindowScene *scene = data.uiwindow.windowScene; 253 CGSize size = { window->pending.w, window->pending.h }; 254 UIWindowSceneGeometryPreferences *preferences = [[UIWindowSceneGeometryPreferencesVision alloc] initWithSize:size]; 255 [scene requestGeometryUpdateWithPreferences:preferences errorHandler:^(NSError * _Nonnull error) { 256 // Request failed, no worries 257 }]; 258 } 259#endif 260} 261 262void UIKit_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) 263{ 264 @autoreleasepool { 265 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; 266 [data.uiwindow makeKeyAndVisible]; 267 268 // Make this window the current mouse focus for touch input 269 SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window); 270 SDL_UIKitDisplayData *displaydata = (__bridge SDL_UIKitDisplayData *)display->internal; 271#ifndef SDL_PLATFORM_VISIONOS 272 if (displaydata.uiscreen == [UIScreen mainScreen]) 273#endif 274 { 275 SDL_SetMouseFocus(window); 276 SDL_SetKeyboardFocus(window); 277 } 278 } 279} 280 281void UIKit_HideWindow(SDL_VideoDevice *_this, SDL_Window *window) 282{ 283 @autoreleasepool { 284 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; 285 data.uiwindow.hidden = YES; 286 } 287} 288 289void UIKit_RaiseWindow(SDL_VideoDevice *_this, SDL_Window *window) 290{ 291#if defined(SDL_VIDEO_OPENGL_ES) || defined(SDL_VIDEO_OPENGL_ES2) 292 /* We don't currently offer a concept of "raising" the SDL window, since 293 * we only allow one per display, in the iOS fashion. 294 * However, we use this entry point to rebind the context to the view 295 * during OnWindowRestored processing. */ 296 _this->GL_MakeCurrent(_this, _this->current_glwin, _this->current_glctx); 297#endif 298} 299 300static void UIKit_UpdateWindowBorder(SDL_VideoDevice *_this, SDL_Window *window) 301{ 302 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; 303 SDL_uikitviewcontroller *viewcontroller = data.viewcontroller; 304 305#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS) 306 if (data.uiwindow.screen == [UIScreen mainScreen]) { 307 if (@available(iOS 13.0, *)) { 308 // iOS 13+ uses view controller's prefersStatusBarHidden 309 } else { 310#pragma clang diagnostic push 311#pragma clang diagnostic ignored "-Wdeprecated-declarations" 312 if (window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS)) { 313 [UIApplication sharedApplication].statusBarHidden = YES; 314 } else { 315 [UIApplication sharedApplication].statusBarHidden = NO; 316 } 317#pragma clang diagnostic pop 318 } 319 320 [viewcontroller setNeedsStatusBarAppearanceUpdate]; 321 } 322 323 // Update the view's frame to account for the status bar change. 324 viewcontroller.view.frame = UIKit_ComputeViewFrame(window, data.uiwindow.screen); 325#endif // !SDL_PLATFORM_TVOS 326 327#ifdef SDL_IPHONE_KEYBOARD 328 // Make sure the view is offset correctly when the keyboard is visible. 329 [viewcontroller updateKeyboard]; 330#endif 331 332 [viewcontroller.view setNeedsLayout]; 333 [viewcontroller.view layoutIfNeeded]; 334} 335 336void UIKit_SetWindowBordered(SDL_VideoDevice *_this, SDL_Window *window, bool bordered) 337{ 338 @autoreleasepool { 339 if (bordered) { 340 window->flags &= ~SDL_WINDOW_BORDERLESS; 341 } else { 342 window->flags |= SDL_WINDOW_BORDERLESS; 343 } 344 UIKit_UpdateWindowBorder(_this, window); 345 } 346} 347 348SDL_FullscreenResult UIKit_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen) 349{ 350 @autoreleasepool { 351 SDL_SendWindowEvent(window, fullscreen ? SDL_EVENT_WINDOW_ENTER_FULLSCREEN : SDL_EVENT_WINDOW_LEAVE_FULLSCREEN, 0, 0); 352 UIKit_UpdateWindowBorder(_this, window); 353 } 354 return SDL_FULLSCREEN_SUCCEEDED; 355} 356 357void UIKit_UpdatePointerLock(SDL_VideoDevice *_this, SDL_Window *window) 358{ 359#ifndef SDL_PLATFORM_TVOS 360 @autoreleasepool { 361 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; 362 SDL_uikitviewcontroller *viewcontroller = data.viewcontroller; 363 if (@available(iOS 14.0, *)) { 364 [viewcontroller setNeedsUpdateOfPrefersPointerLocked]; 365 } 366 } 367#endif // !SDL_PLATFORM_TVOS 368} 369 370void UIKit_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window) 371{ 372 @autoreleasepool { 373 if (window->internal != NULL) { 374 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; 375 NSArray *views = nil; 376 377 [data.viewcontroller stopAnimation]; 378 379 /* Detach all views from this window. We use a copy of the array 380 * because setSDLWindow will remove the object from the original 381 * array, which would be undesirable if we were iterating over it. */ 382 views = [data.views copy]; 383 for (SDL_uikitview *view in views) { 384 [view setSDLWindow:NULL]; 385 } 386 387 /* iOS may still hold a reference to the window after we release it. 388 * We want to make sure the SDL view controller isn't accessed in 389 * that case, because it would contain an invalid pointer to the old 390 * SDL window. */ 391 data.uiwindow.rootViewController = nil; 392 data.uiwindow.hidden = YES; 393 394 CFRelease(window->internal); 395 window->internal = NULL; 396 } 397 } 398} 399 400void UIKit_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h) 401{ 402 @autoreleasepool { 403 SDL_UIKitWindowData *windata = (__bridge SDL_UIKitWindowData *)window->internal; 404 UIView *view = windata.viewcontroller.view; 405 CGSize size = view.bounds.size; 406 CGFloat scale = 1.0; 407 408 409 if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) { 410#ifndef SDL_PLATFORM_VISIONOS 411 scale = windata.uiwindow.screen.nativeScale; 412#else 413 scale = 2.0; 414#endif 415 } 416 417 418 /* Integer truncation of fractional values matches SDL_uikitmetalview and 419 * SDL_uikitopenglview. */ 420 *w = (int)(size.width * scale); 421 *h = (int)(size.height * scale); 422 } 423} 424 425#ifndef SDL_PLATFORM_TVOS 426NSUInteger 427UIKit_GetSupportedOrientations(SDL_Window *window) 428{ 429 const char *hint = SDL_GetHint(SDL_HINT_ORIENTATIONS); 430 NSUInteger validOrientations = UIInterfaceOrientationMaskAll; 431 NSUInteger orientationMask = 0; 432 433 @autoreleasepool { 434 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; 435 UIApplication *app = [UIApplication sharedApplication]; 436 437 /* Get all possible valid orientations. If the app delegate doesn't tell 438 * us, we get the orientations from Info.plist via UIApplication. */ 439 if ([app.delegate respondsToSelector:@selector(application:supportedInterfaceOrientationsForWindow:)]) { 440 validOrientations = [app.delegate application:app supportedInterfaceOrientationsForWindow:data.uiwindow]; 441 } else { 442 validOrientations = [app supportedInterfaceOrientationsForWindow:data.uiwindow]; 443 } 444 445 if (hint != NULL) { 446 NSArray *orientations = [@(hint) componentsSeparatedByString:@" "]; 447 448 if ([orientations containsObject:@"LandscapeLeft"]) { 449 orientationMask |= UIInterfaceOrientationMaskLandscapeLeft; 450 } 451 if ([orientations containsObject:@"LandscapeRight"]) { 452 orientationMask |= UIInterfaceOrientationMaskLandscapeRight; 453 } 454 if ([orientations containsObject:@"Portrait"]) { 455 orientationMask |= UIInterfaceOrientationMaskPortrait; 456 } 457 if ([orientations containsObject:@"PortraitUpsideDown"]) { 458 orientationMask |= UIInterfaceOrientationMaskPortraitUpsideDown; 459 } 460 } 461 462 if (orientationMask == 0 && (window->flags & SDL_WINDOW_RESIZABLE)) { 463 // any orientation is okay. 464 orientationMask = UIInterfaceOrientationMaskAll; 465 } 466 467 if (orientationMask == 0) { 468 if (window->floating.w >= window->floating.h) { 469 orientationMask |= UIInterfaceOrientationMaskLandscape; 470 } 471 if (window->floating.h >= window->floating.w) { 472 orientationMask |= (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown); 473 } 474 } 475 476 // Don't allow upside-down orientation on phones, so answering calls is in the natural orientation 477 if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) { 478 orientationMask &= ~UIInterfaceOrientationMaskPortraitUpsideDown; 479 } 480 481 /* If none of the specified orientations are actually supported by the 482 * app, we'll revert to what the app supports. An exception would be 483 * thrown by the system otherwise. */ 484 if ((validOrientations & orientationMask) == 0) { 485 orientationMask = validOrientations; 486 } 487 } 488 489 return orientationMask; 490} 491#endif // !SDL_PLATFORM_TVOS 492 493bool SDL_SetiOSAnimationCallback(SDL_Window *window, int interval, SDL_iOSAnimationCallback callback, void *callbackParam) 494{ 495 if (!window || !window->internal) { 496 return SDL_SetError("Invalid window"); 497 } 498 499 @autoreleasepool { 500 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal; 501 [data.viewcontroller setAnimationCallback:interval 502 callback:callback 503 callbackParam:callbackParam]; 504 } 505 506 return true; 507} 508 509#endif // SDL_VIDEO_DRIVER_UIKIT 510
[FILE END]
(C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.