summaryrefslogtreecommitdiff
path: root/contrib/SDL-3.2.8/src/video/uikit/SDL_uikitwindow.m
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2025-12-27 12:03:39 -0800
committer3gg <3gg@shellblade.net>2025-12-27 12:03:39 -0800
commit5a079a2d114f96d4847d1ee305d5b7c16eeec50e (patch)
tree8926ab44f168acf787d8e19608857b3af0f82758 /contrib/SDL-3.2.8/src/video/uikit/SDL_uikitwindow.m
Initial commit
Diffstat (limited to 'contrib/SDL-3.2.8/src/video/uikit/SDL_uikitwindow.m')
-rw-r--r--contrib/SDL-3.2.8/src/video/uikit/SDL_uikitwindow.m471
1 files changed, 471 insertions, 0 deletions
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 @@
1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
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
56@interface SDL_uikitwindow : UIWindow
57
58- (void)layoutSubviews;
59
60@end
61
62@implementation SDL_uikitwindow
63
64- (void)layoutSubviews
65{
66#ifndef SDL_PLATFORM_VISIONOS
67 // Workaround to fix window orientation issues in iOS 8.
68 /* As of July 1 2019, I haven't been able to reproduce any orientation
69 * issues with this disabled on iOS 12. The issue this is meant to fix might
70 * only happen on iOS 8, or it might have been fixed another way with other
71 * code... This code prevents split view (iOS 9+) from working on iPads, so
72 * we want to avoid using it if possible. */
73 if (!UIKit_IsSystemVersionAtLeast(9.0)) {
74 self.frame = self.screen.bounds;
75 }
76#endif
77 [super layoutSubviews];
78}
79
80@end
81
82static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow *uiwindow, bool created)
83{
84 SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window);
85 SDL_UIKitDisplayData *displaydata = (__bridge SDL_UIKitDisplayData *)display->internal;
86 SDL_uikitview *view;
87
88#ifdef SDL_PLATFORM_VISIONOS
89 CGRect frame = UIKit_ComputeViewFrame(window);
90#else
91 CGRect frame = UIKit_ComputeViewFrame(window, displaydata.uiscreen);
92#endif
93
94 int width = (int)frame.size.width;
95 int height = (int)frame.size.height;
96
97 SDL_UIKitWindowData *data = [[SDL_UIKitWindowData alloc] init];
98 if (!data) {
99 return SDL_OutOfMemory();
100 }
101
102 window->internal = (SDL_WindowData *)CFBridgingRetain(data);
103
104 data.uiwindow = uiwindow;
105
106#ifndef SDL_PLATFORM_VISIONOS
107 if (displaydata.uiscreen != [UIScreen mainScreen]) {
108 window->flags &= ~SDL_WINDOW_RESIZABLE; // window is NEVER resizable
109 window->flags &= ~SDL_WINDOW_INPUT_FOCUS; // never has input focus
110 window->flags |= SDL_WINDOW_BORDERLESS; // never has a status bar.
111 }
112#endif
113
114#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS)
115 if (displaydata.uiscreen == [UIScreen mainScreen]) {
116 NSUInteger orients = UIKit_GetSupportedOrientations(window);
117 BOOL supportsLandscape = (orients & UIInterfaceOrientationMaskLandscape) != 0;
118 BOOL supportsPortrait = (orients & (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown)) != 0;
119
120 // Make sure the width/height are oriented correctly
121 if ((width > height && !supportsLandscape) || (height > width && !supportsPortrait)) {
122 int temp = width;
123 width = height;
124 height = temp;
125 }
126 }
127#endif // !SDL_PLATFORM_TVOS
128
129#if 0 // Don't set the x/y position, it's already placed on a display
130 window->x = 0;
131 window->y = 0;
132#endif
133 window->w = width;
134 window->h = height;
135
136 /* The View Controller will handle rotating the view when the device
137 * orientation changes. This will trigger resize events, if appropriate. */
138 data.viewcontroller = [[SDL_uikitviewcontroller alloc] initWithSDLWindow:window];
139
140 /* The window will initially contain a generic view so resizes, touch events,
141 * etc. can be handled without an active OpenGL view/context. */
142 view = [[SDL_uikitview alloc] initWithFrame:frame];
143
144 /* Sets this view as the controller's view, and adds the view to the window
145 * hierarchy. */
146 [view setSDLWindow:window];
147
148 SDL_PropertiesID props = SDL_GetWindowProperties(window);
149 SDL_SetPointerProperty(props, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, (__bridge void *)data.uiwindow);
150 SDL_SetNumberProperty(props, SDL_PROP_WINDOW_UIKIT_METAL_VIEW_TAG_NUMBER, SDL_METALVIEW_TAG);
151
152 return true;
153}
154
155bool UIKit_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props)
156{
157 @autoreleasepool {
158 SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window);
159 SDL_UIKitDisplayData *data = (__bridge SDL_UIKitDisplayData *)display->internal;
160 SDL_Window *other;
161
162 // We currently only handle a single window per display on iOS
163 for (other = _this->windows; other; other = other->next) {
164 if (other != window && SDL_GetVideoDisplayForWindow(other) == display) {
165 return SDL_SetError("Only one window allowed per display.");
166 }
167 }
168
169 /* If monitor has a resolution of 0x0 (hasn't been explicitly set by the
170 * user, so it's in standby), try to force the display to a resolution
171 * that most closely matches the desired window size. */
172#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS)
173 const CGSize origsize = data.uiscreen.currentMode.size;
174 if ((origsize.width == 0.0f) && (origsize.height == 0.0f)) {
175 SDL_DisplayMode bestmode;
176 bool include_high_density_modes = false;
177 if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) {
178 include_high_density_modes = true;
179 }
180 if (SDL_GetClosestFullscreenDisplayMode(display->id, window->w, window->h, 0.0f, include_high_density_modes, &bestmode)) {
181 SDL_UIKitDisplayModeData *modedata = (__bridge SDL_UIKitDisplayModeData *)bestmode.internal;
182 [data.uiscreen setCurrentMode:modedata.uiscreenmode];
183
184 /* desktop_mode doesn't change here (the higher level will
185 * use it to set all the screens back to their defaults
186 * upon window destruction, SDL_Quit(), etc. */
187 SDL_SetCurrentDisplayMode(display, &bestmode);
188 }
189 }
190
191 if (data.uiscreen == [UIScreen mainScreen]) {
192 if (window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS)) {
193 [UIApplication sharedApplication].statusBarHidden = YES;
194 } else {
195 [UIApplication sharedApplication].statusBarHidden = NO;
196 }
197 }
198#endif // !SDL_PLATFORM_TVOS
199
200 // ignore the size user requested, and make a fullscreen window
201 // !!! FIXME: can we have a smaller view?
202#ifdef SDL_PLATFORM_VISIONOS
203 UIWindow *uiwindow = [[SDL_uikitwindow alloc] initWithFrame:CGRectMake(window->x, window->y, window->w, window->h)];
204#else
205 UIWindow *uiwindow = [[SDL_uikitwindow alloc] initWithFrame:data.uiscreen.bounds];
206#endif
207
208 // put the window on an external display if appropriate.
209#ifndef SDL_PLATFORM_VISIONOS
210 if (data.uiscreen != [UIScreen mainScreen]) {
211 [uiwindow setScreen:data.uiscreen];
212 }
213#endif
214
215 if (!SetupWindowData(_this, window, uiwindow, true)) {
216 return false;
217 }
218 }
219
220 return true;
221}
222
223void UIKit_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window)
224{
225 @autoreleasepool {
226 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
227 data.viewcontroller.title = @(window->title);
228 }
229}
230
231void UIKit_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
232{
233 @autoreleasepool {
234 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
235 [data.uiwindow makeKeyAndVisible];
236
237 // Make this window the current mouse focus for touch input
238 SDL_VideoDisplay *display = SDL_GetVideoDisplayForWindow(window);
239 SDL_UIKitDisplayData *displaydata = (__bridge SDL_UIKitDisplayData *)display->internal;
240#ifndef SDL_PLATFORM_VISIONOS
241 if (displaydata.uiscreen == [UIScreen mainScreen])
242#endif
243 {
244 SDL_SetMouseFocus(window);
245 SDL_SetKeyboardFocus(window);
246 }
247 }
248}
249
250void UIKit_HideWindow(SDL_VideoDevice *_this, SDL_Window *window)
251{
252 @autoreleasepool {
253 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
254 data.uiwindow.hidden = YES;
255 }
256}
257
258void UIKit_RaiseWindow(SDL_VideoDevice *_this, SDL_Window *window)
259{
260#if defined(SDL_VIDEO_OPENGL_ES) || defined(SDL_VIDEO_OPENGL_ES2)
261 /* We don't currently offer a concept of "raising" the SDL window, since
262 * we only allow one per display, in the iOS fashion.
263 * However, we use this entry point to rebind the context to the view
264 * during OnWindowRestored processing. */
265 _this->GL_MakeCurrent(_this, _this->current_glwin, _this->current_glctx);
266#endif
267}
268
269static void UIKit_UpdateWindowBorder(SDL_VideoDevice *_this, SDL_Window *window)
270{
271 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
272 SDL_uikitviewcontroller *viewcontroller = data.viewcontroller;
273
274#if !defined(SDL_PLATFORM_TVOS) && !defined(SDL_PLATFORM_VISIONOS)
275 if (data.uiwindow.screen == [UIScreen mainScreen]) {
276 if (window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS)) {
277 [UIApplication sharedApplication].statusBarHidden = YES;
278 } else {
279 [UIApplication sharedApplication].statusBarHidden = NO;
280 }
281
282 [viewcontroller setNeedsStatusBarAppearanceUpdate];
283 }
284
285 // Update the view's frame to account for the status bar change.
286 viewcontroller.view.frame = UIKit_ComputeViewFrame(window, data.uiwindow.screen);
287#endif // !SDL_PLATFORM_TVOS
288
289#ifdef SDL_IPHONE_KEYBOARD
290 // Make sure the view is offset correctly when the keyboard is visible.
291 [viewcontroller updateKeyboard];
292#endif
293
294 [viewcontroller.view setNeedsLayout];
295 [viewcontroller.view layoutIfNeeded];
296}
297
298void UIKit_SetWindowBordered(SDL_VideoDevice *_this, SDL_Window *window, bool bordered)
299{
300 @autoreleasepool {
301 if (bordered) {
302 window->flags &= ~SDL_WINDOW_BORDERLESS;
303 } else {
304 window->flags |= SDL_WINDOW_BORDERLESS;
305 }
306 UIKit_UpdateWindowBorder(_this, window);
307 }
308}
309
310SDL_FullscreenResult UIKit_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window, SDL_VideoDisplay *display, SDL_FullscreenOp fullscreen)
311{
312 @autoreleasepool {
313 SDL_SendWindowEvent(window, fullscreen ? SDL_EVENT_WINDOW_ENTER_FULLSCREEN : SDL_EVENT_WINDOW_LEAVE_FULLSCREEN, 0, 0);
314 UIKit_UpdateWindowBorder(_this, window);
315 }
316 return SDL_FULLSCREEN_SUCCEEDED;
317}
318
319void UIKit_UpdatePointerLock(SDL_VideoDevice *_this, SDL_Window *window)
320{
321#ifndef SDL_PLATFORM_TVOS
322 @autoreleasepool {
323 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
324 SDL_uikitviewcontroller *viewcontroller = data.viewcontroller;
325 if (@available(iOS 14.0, *)) {
326 [viewcontroller setNeedsUpdateOfPrefersPointerLocked];
327 }
328 }
329#endif // !SDL_PLATFORM_TVOS
330}
331
332void UIKit_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window)
333{
334 @autoreleasepool {
335 if (window->internal != NULL) {
336 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
337 NSArray *views = nil;
338
339 [data.viewcontroller stopAnimation];
340
341 /* Detach all views from this window. We use a copy of the array
342 * because setSDLWindow will remove the object from the original
343 * array, which would be undesirable if we were iterating over it. */
344 views = [data.views copy];
345 for (SDL_uikitview *view in views) {
346 [view setSDLWindow:NULL];
347 }
348
349 /* iOS may still hold a reference to the window after we release it.
350 * We want to make sure the SDL view controller isn't accessed in
351 * that case, because it would contain an invalid pointer to the old
352 * SDL window. */
353 data.uiwindow.rootViewController = nil;
354 data.uiwindow.hidden = YES;
355
356 CFRelease(window->internal);
357 window->internal = NULL;
358 }
359 }
360}
361
362void UIKit_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h)
363{
364 @autoreleasepool {
365 SDL_UIKitWindowData *windata = (__bridge SDL_UIKitWindowData *)window->internal;
366 UIView *view = windata.viewcontroller.view;
367 CGSize size = view.bounds.size;
368 CGFloat scale = 1.0;
369
370
371 if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) {
372#ifndef SDL_PLATFORM_VISIONOS
373 scale = windata.uiwindow.screen.nativeScale;
374#else
375 scale = 2.0;
376#endif
377 }
378
379
380 /* Integer truncation of fractional values matches SDL_uikitmetalview and
381 * SDL_uikitopenglview. */
382 *w = (int)(size.width * scale);
383 *h = (int)(size.height * scale);
384 }
385}
386
387#ifndef SDL_PLATFORM_TVOS
388NSUInteger
389UIKit_GetSupportedOrientations(SDL_Window *window)
390{
391 const char *hint = SDL_GetHint(SDL_HINT_ORIENTATIONS);
392 NSUInteger validOrientations = UIInterfaceOrientationMaskAll;
393 NSUInteger orientationMask = 0;
394
395 @autoreleasepool {
396 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
397 UIApplication *app = [UIApplication sharedApplication];
398
399 /* Get all possible valid orientations. If the app delegate doesn't tell
400 * us, we get the orientations from Info.plist via UIApplication. */
401 if ([app.delegate respondsToSelector:@selector(application:supportedInterfaceOrientationsForWindow:)]) {
402 validOrientations = [app.delegate application:app supportedInterfaceOrientationsForWindow:data.uiwindow];
403 } else {
404 validOrientations = [app supportedInterfaceOrientationsForWindow:data.uiwindow];
405 }
406
407 if (hint != NULL) {
408 NSArray *orientations = [@(hint) componentsSeparatedByString:@" "];
409
410 if ([orientations containsObject:@"LandscapeLeft"]) {
411 orientationMask |= UIInterfaceOrientationMaskLandscapeLeft;
412 }
413 if ([orientations containsObject:@"LandscapeRight"]) {
414 orientationMask |= UIInterfaceOrientationMaskLandscapeRight;
415 }
416 if ([orientations containsObject:@"Portrait"]) {
417 orientationMask |= UIInterfaceOrientationMaskPortrait;
418 }
419 if ([orientations containsObject:@"PortraitUpsideDown"]) {
420 orientationMask |= UIInterfaceOrientationMaskPortraitUpsideDown;
421 }
422 }
423
424 if (orientationMask == 0 && (window->flags & SDL_WINDOW_RESIZABLE)) {
425 // any orientation is okay.
426 orientationMask = UIInterfaceOrientationMaskAll;
427 }
428
429 if (orientationMask == 0) {
430 if (window->floating.w >= window->floating.h) {
431 orientationMask |= UIInterfaceOrientationMaskLandscape;
432 }
433 if (window->floating.h >= window->floating.w) {
434 orientationMask |= (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown);
435 }
436 }
437
438 // Don't allow upside-down orientation on phones, so answering calls is in the natural orientation
439 if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) {
440 orientationMask &= ~UIInterfaceOrientationMaskPortraitUpsideDown;
441 }
442
443 /* If none of the specified orientations are actually supported by the
444 * app, we'll revert to what the app supports. An exception would be
445 * thrown by the system otherwise. */
446 if ((validOrientations & orientationMask) == 0) {
447 orientationMask = validOrientations;
448 }
449 }
450
451 return orientationMask;
452}
453#endif // !SDL_PLATFORM_TVOS
454
455bool SDL_SetiOSAnimationCallback(SDL_Window *window, int interval, SDL_iOSAnimationCallback callback, void *callbackParam)
456{
457 if (!window || !window->internal) {
458 return SDL_SetError("Invalid window");
459 }
460
461 @autoreleasepool {
462 SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
463 [data.viewcontroller setAnimationCallback:interval
464 callback:callback
465 callbackParam:callbackParam];
466 }
467
468 return true;
469}
470
471#endif // SDL_VIDEO_DRIVER_UIKIT