Imported Upstream version 2.0.14
[platform/upstream/SDL.git] / src / video / uikit / SDL_uikitmodes.m
1 /*
2   Simple DirectMedia Layer
3   Copyright (C) 1997-2020 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 #if SDL_VIDEO_DRIVER_UIKIT
24
25 #include "SDL_system.h"
26 #include "SDL_uikitmodes.h"
27
28 #include "../../events/SDL_events_c.h"
29
30 #import <sys/utsname.h>
31
32 @implementation SDL_DisplayData
33
34 - (instancetype)initWithScreen:(UIScreen*)screen
35 {
36     if (self = [super init]) {
37         self.uiscreen = screen;
38
39         /*
40          * A well up to date list of device info can be found here:
41          * https://github.com/lmirosevic/GBDeviceInfo/blob/master/GBDeviceInfo/GBDeviceInfo_iOS.m
42          */
43         NSDictionary* devices = @{
44             @"iPhone1,1": @163,
45             @"iPhone1,2": @163,
46             @"iPhone2,1": @163,
47             @"iPhone3,1": @326,
48             @"iPhone3,2": @326,
49             @"iPhone3,3": @326,
50             @"iPhone4,1": @326,
51             @"iPhone5,1": @326,
52             @"iPhone5,2": @326,
53             @"iPhone5,3": @326,
54             @"iPhone5,4": @326,
55             @"iPhone6,1": @326,
56             @"iPhone6,2": @326,
57             @"iPhone7,1": @401,
58             @"iPhone7,2": @326,
59             @"iPhone8,1": @326,
60             @"iPhone8,2": @401,
61             @"iPhone8,4": @326,
62             @"iPhone9,1": @326,
63             @"iPhone9,2": @401,
64             @"iPhone9,3": @326,
65             @"iPhone9,4": @401,
66             @"iPhone10,1": @326,
67             @"iPhone10,2": @401,
68             @"iPhone10,3": @458,
69             @"iPhone10,4": @326,
70             @"iPhone10,5": @401,
71             @"iPhone10,6": @458,
72             @"iPhone11,2": @458,
73             @"iPhone11,4": @458,
74             @"iPhone11,6": @458,
75             @"iPhone11,8": @326,
76             @"iPhone12,1": @326,
77             @"iPhone12,3": @458,
78             @"iPhone12,5": @458,
79             @"iPad1,1": @132,
80             @"iPad2,1": @132,
81             @"iPad2,2": @132,
82             @"iPad2,3": @132,
83             @"iPad2,4": @132,
84             @"iPad2,5": @163,
85             @"iPad2,6": @163,
86             @"iPad2,7": @163,
87             @"iPad3,1": @264,
88             @"iPad3,2": @264,
89             @"iPad3,3": @264,
90             @"iPad3,4": @264,
91             @"iPad3,5": @264,
92             @"iPad3,6": @264,
93             @"iPad4,1": @264,
94             @"iPad4,2": @264,
95             @"iPad4,3": @264,
96             @"iPad4,4": @326,
97             @"iPad4,5": @326,
98             @"iPad4,6": @326,
99             @"iPad4,7": @326,
100             @"iPad4,8": @326,
101             @"iPad4,9": @326,
102             @"iPad5,1": @326,
103             @"iPad5,2": @326,
104             @"iPad5,3": @264,
105             @"iPad5,4": @264,
106             @"iPad6,3": @264,
107             @"iPad6,4": @264,
108             @"iPad6,7": @264,
109             @"iPad6,8": @264,
110             @"iPad6,11": @264,
111             @"iPad6,12": @264,
112             @"iPad7,1": @264,
113             @"iPad7,2": @264,
114             @"iPad7,3": @264,
115             @"iPad7,4": @264,
116             @"iPad7,5": @264,
117             @"iPad7,6": @264,
118             @"iPad7,11": @264,
119             @"iPad7,12": @264,
120             @"iPad8,1": @264,
121             @"iPad8,2": @264,
122             @"iPad8,3": @264,
123             @"iPad8,4": @264,
124             @"iPad8,5": @264,
125             @"iPad8,6": @264,
126             @"iPad8,7": @264,
127             @"iPad8,8": @264,
128             @"iPad11,1": @326,
129             @"iPad11,2": @326,
130             @"iPad11,3": @326,
131             @"iPad11,4": @326,
132             @"iPod1,1": @163,
133             @"iPod2,1": @163,
134             @"iPod3,1": @163,
135             @"iPod4,1": @326,
136             @"iPod5,1": @326,
137             @"iPod7,1": @326,
138             @"iPod9,1": @326,
139         };
140
141         struct utsname systemInfo;
142         uname(&systemInfo);
143         NSString* deviceName =
144             [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
145         id foundDPI = devices[deviceName];
146         if (foundDPI) {
147             self.screenDPI = (float)[foundDPI integerValue];
148         } else {
149             /*
150              * Estimate the DPI based on the screen scale multiplied by the base DPI for the device
151              * type (e.g. based on iPhone 1 and iPad 1)
152              */
153     #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 80000
154             float scale = (float)screen.nativeScale;
155     #else
156             float scale = (float)screen.scale;
157     #endif
158             float defaultDPI;
159             if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
160                 defaultDPI = 132.0f;
161             } else if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
162                 defaultDPI = 163.0f;
163             } else {
164                 defaultDPI = 160.0f;
165             }
166             self.screenDPI = scale * defaultDPI;
167         }
168     }
169     return self;
170 }
171
172 @synthesize uiscreen;
173 @synthesize screenDPI;
174
175 @end
176
177 @implementation SDL_DisplayModeData
178
179 @synthesize uiscreenmode;
180
181 @end
182
183 @interface SDL_DisplayWatch : NSObject
184 @end
185
186 @implementation SDL_DisplayWatch
187
188 + (void)start
189 {
190     NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
191
192     [center addObserver:self selector:@selector(screenConnected:)
193             name:UIScreenDidConnectNotification object:nil];
194     [center addObserver:self selector:@selector(screenDisconnected:)
195             name:UIScreenDidDisconnectNotification object:nil];
196 }
197
198 + (void)stop
199 {
200     NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
201
202     [center removeObserver:self
203             name:UIScreenDidConnectNotification object:nil];
204     [center removeObserver:self
205             name:UIScreenDidDisconnectNotification object:nil];
206 }
207
208 + (void)screenConnected:(NSNotification*)notification
209 {
210     UIScreen *uiscreen = [notification object];
211     UIKit_AddDisplay(uiscreen, SDL_TRUE);
212 }
213
214 + (void)screenDisconnected:(NSNotification*)notification
215 {
216     UIScreen *uiscreen = [notification object];
217     UIKit_DelDisplay(uiscreen);
218 }
219
220 @end
221
222 static int
223 UIKit_AllocateDisplayModeData(SDL_DisplayMode * mode,
224     UIScreenMode * uiscreenmode)
225 {
226     SDL_DisplayModeData *data = nil;
227
228     if (uiscreenmode != nil) {
229         /* Allocate the display mode data */
230         data = [[SDL_DisplayModeData alloc] init];
231         if (!data) {
232             return SDL_OutOfMemory();
233         }
234
235         data.uiscreenmode = uiscreenmode;
236     }
237
238     mode->driverdata = (void *) CFBridgingRetain(data);
239
240     return 0;
241 }
242
243 static void
244 UIKit_FreeDisplayModeData(SDL_DisplayMode * mode)
245 {
246     if (mode->driverdata != NULL) {
247         CFRelease(mode->driverdata);
248         mode->driverdata = NULL;
249     }
250 }
251
252 static NSUInteger
253 UIKit_GetDisplayModeRefreshRate(UIScreen *uiscreen)
254 {
255 #ifdef __IPHONE_10_3
256     if ([uiscreen respondsToSelector:@selector(maximumFramesPerSecond)]) {
257         return uiscreen.maximumFramesPerSecond;
258     }
259 #endif
260     return 0;
261 }
262
263 static int
264 UIKit_AddSingleDisplayMode(SDL_VideoDisplay * display, int w, int h,
265     UIScreen * uiscreen, UIScreenMode * uiscreenmode)
266 {
267     SDL_DisplayMode mode;
268     SDL_zero(mode);
269
270     if (UIKit_AllocateDisplayModeData(&mode, uiscreenmode) < 0) {
271         return -1;
272     }
273
274     mode.format = SDL_PIXELFORMAT_ABGR8888;
275     mode.refresh_rate = (int) UIKit_GetDisplayModeRefreshRate(uiscreen);
276     mode.w = w;
277     mode.h = h;
278
279     if (SDL_AddDisplayMode(display, &mode)) {
280         return 0;
281     } else {
282         UIKit_FreeDisplayModeData(&mode);
283         return -1;
284     }
285 }
286
287 static int
288 UIKit_AddDisplayMode(SDL_VideoDisplay * display, int w, int h, UIScreen * uiscreen,
289                      UIScreenMode * uiscreenmode, SDL_bool addRotation)
290 {
291     if (UIKit_AddSingleDisplayMode(display, w, h, uiscreen, uiscreenmode) < 0) {
292         return -1;
293     }
294
295     if (addRotation) {
296         /* Add the rotated version */
297         if (UIKit_AddSingleDisplayMode(display, h, w, uiscreen, uiscreenmode) < 0) {
298             return -1;
299         }
300     }
301
302     return 0;
303 }
304
305 int
306 UIKit_AddDisplay(UIScreen *uiscreen, SDL_bool send_event)
307 {
308     UIScreenMode *uiscreenmode = uiscreen.currentMode;
309     CGSize size = uiscreen.bounds.size;
310     SDL_VideoDisplay display;
311     SDL_DisplayMode mode;
312     SDL_zero(mode);
313
314     /* Make sure the width/height are oriented correctly */
315     if (UIKit_IsDisplayLandscape(uiscreen) != (size.width > size.height)) {
316         CGFloat height = size.width;
317         size.width = size.height;
318         size.height = height;
319     }
320
321     mode.format = SDL_PIXELFORMAT_ABGR8888;
322     mode.refresh_rate = (int) UIKit_GetDisplayModeRefreshRate(uiscreen);
323     mode.w = (int) size.width;
324     mode.h = (int) size.height;
325
326     if (UIKit_AllocateDisplayModeData(&mode, uiscreenmode) < 0) {
327         return -1;
328     }
329
330     SDL_zero(display);
331     display.desktop_mode = mode;
332     display.current_mode = mode;
333
334     /* Allocate the display data */
335     SDL_DisplayData *data = [[SDL_DisplayData alloc] initWithScreen:uiscreen];
336     if (!data) {
337         UIKit_FreeDisplayModeData(&display.desktop_mode);
338         return SDL_OutOfMemory();
339     }
340
341     display.driverdata = (void *) CFBridgingRetain(data);
342     SDL_AddVideoDisplay(&display, send_event);
343
344     return 0;
345 }
346
347 void
348 UIKit_DelDisplay(UIScreen *uiscreen)
349 {
350     int i;
351
352     for (i = 0; i < SDL_GetNumVideoDisplays(); ++i) {
353         SDL_DisplayData *data = (__bridge SDL_DisplayData *)SDL_GetDisplayDriverData(i);
354
355         if (data && data.uiscreen == uiscreen) {
356             CFRelease(SDL_GetDisplayDriverData(i));
357             SDL_DelVideoDisplay(i);
358             return;
359         }
360     }
361 }
362
363 SDL_bool
364 UIKit_IsDisplayLandscape(UIScreen *uiscreen)
365 {
366 #if !TARGET_OS_TV
367     if (uiscreen == [UIScreen mainScreen]) {
368         return UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation);
369     } else
370 #endif /* !TARGET_OS_TV */
371     {
372         CGSize size = uiscreen.bounds.size;
373         return (size.width > size.height);
374     }
375 }
376
377 int
378 UIKit_InitModes(_THIS)
379 {
380     @autoreleasepool {
381         for (UIScreen *uiscreen in [UIScreen screens]) {
382             if (UIKit_AddDisplay(uiscreen, SDL_FALSE) < 0) {
383                 return -1;
384             }
385         }
386 #if !TARGET_OS_TV
387         SDL_OnApplicationDidChangeStatusBarOrientation();
388 #endif
389
390         [SDL_DisplayWatch start];
391     }
392
393     return 0;
394 }
395
396 void
397 UIKit_GetDisplayModes(_THIS, SDL_VideoDisplay * display)
398 {
399     @autoreleasepool {
400         SDL_DisplayData *data = (__bridge SDL_DisplayData *) display->driverdata;
401
402         SDL_bool isLandscape = UIKit_IsDisplayLandscape(data.uiscreen);
403         SDL_bool addRotation = (data.uiscreen == [UIScreen mainScreen]);
404         CGFloat scale = data.uiscreen.scale;
405         NSArray *availableModes = nil;
406
407 #if TARGET_OS_TV
408         addRotation = SDL_FALSE;
409         availableModes = @[data.uiscreen.currentMode];
410 #else
411         availableModes = data.uiscreen.availableModes;
412 #endif
413
414         for (UIScreenMode *uimode in availableModes) {
415             /* The size of a UIScreenMode is in pixels, but we deal exclusively
416              * in points (except in SDL_GL_GetDrawableSize.)
417              *
418              * For devices such as iPhone 6/7/8 Plus, the UIScreenMode reported
419              * by iOS is not in physical pixels of the display, but rather the
420              * point size times the scale.  For example, on iOS 12.2 on iPhone 8
421              * Plus the uimode.size is 1242x2208 and the uiscreen.scale is 3
422              * thus this will give the size in points which is 414x736. The code
423              * used to use the nativeScale, assuming UIScreenMode returned raw
424              * physical pixels (as suggested by its documentation, but in
425              * practice it is returning the retina pixels). */
426             int w = (int)(uimode.size.width / scale);
427             int h = (int)(uimode.size.height / scale);
428
429             /* Make sure the width/height are oriented correctly */
430             if (isLandscape != (w > h)) {
431                 int tmp = w;
432                 w = h;
433                 h = tmp;
434             }
435
436             UIKit_AddDisplayMode(display, w, h, data.uiscreen, uimode, addRotation);
437         }
438     }
439 }
440
441 int
442 UIKit_GetDisplayDPI(_THIS, SDL_VideoDisplay * display, float * ddpi, float * hdpi, float * vdpi)
443 {
444     @autoreleasepool {
445         SDL_DisplayData *data = (__bridge SDL_DisplayData *) display->driverdata;
446         float dpi = data.screenDPI;
447
448         if (ddpi) {
449             *ddpi = dpi * (float)SDL_sqrt(2.0);
450         }
451         if (hdpi) {
452             *hdpi = dpi;
453         }
454         if (vdpi) {
455             *vdpi = dpi;
456         }
457     }
458
459     return 0;
460 }
461
462 int
463 UIKit_SetDisplayMode(_THIS, SDL_VideoDisplay * display, SDL_DisplayMode * mode)
464 {
465     @autoreleasepool {
466         SDL_DisplayData *data = (__bridge SDL_DisplayData *) display->driverdata;
467
468 #if !TARGET_OS_TV
469         SDL_DisplayModeData *modedata = (__bridge SDL_DisplayModeData *)mode->driverdata;
470         [data.uiscreen setCurrentMode:modedata.uiscreenmode];
471 #endif
472
473         if (data.uiscreen == [UIScreen mainScreen]) {
474             /* [UIApplication setStatusBarOrientation:] no longer works reliably
475              * in recent iOS versions, so we can't rotate the screen when setting
476              * the display mode. */
477             if (mode->w > mode->h) {
478                 if (!UIKit_IsDisplayLandscape(data.uiscreen)) {
479                     return SDL_SetError("Screen orientation does not match display mode size");
480                 }
481             } else if (mode->w < mode->h) {
482                 if (UIKit_IsDisplayLandscape(data.uiscreen)) {
483                     return SDL_SetError("Screen orientation does not match display mode size");
484                 }
485             }
486         }
487     }
488
489     return 0;
490 }
491
492 int
493 UIKit_GetDisplayUsableBounds(_THIS, SDL_VideoDisplay * display, SDL_Rect * rect)
494 {
495     @autoreleasepool {
496         int displayIndex = (int) (display - _this->displays);
497         SDL_DisplayData *data = (__bridge SDL_DisplayData *) display->driverdata;
498         CGRect frame = data.uiscreen.bounds;
499
500         /* the default function iterates displays to make a fake offset,
501          as if all the displays were side-by-side, which is fine for iOS. */
502         if (SDL_GetDisplayBounds(displayIndex, rect) < 0) {
503             return -1;
504         }
505
506 #if !TARGET_OS_TV && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0
507         if (!UIKit_IsSystemVersionAtLeast(7.0)) {
508             frame = [data.uiscreen applicationFrame];
509         }
510 #endif
511
512         rect->x += frame.origin.x;
513         rect->y += frame.origin.y;
514         rect->w = frame.size.width;
515         rect->h = frame.size.height;
516     }
517
518     return 0;
519 }
520
521 void
522 UIKit_QuitModes(_THIS)
523 {
524     [SDL_DisplayWatch stop];
525
526     /* Release Objective-C objects, so higher level doesn't free() them. */
527     int i, j;
528     @autoreleasepool {
529         for (i = 0; i < _this->num_displays; i++) {
530             SDL_VideoDisplay *display = &_this->displays[i];
531
532             UIKit_FreeDisplayModeData(&display->desktop_mode);
533             for (j = 0; j < display->num_display_modes; j++) {
534                 SDL_DisplayMode *mode = &display->display_modes[j];
535                 UIKit_FreeDisplayModeData(mode);
536             }
537
538             if (display->driverdata != NULL) {
539                 CFRelease(display->driverdata);
540                 display->driverdata = NULL;
541             }
542         }
543     }
544 }
545
546 #if !TARGET_OS_TV
547 void SDL_OnApplicationDidChangeStatusBarOrientation()
548 {
549     BOOL isLandscape = UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation);
550     SDL_VideoDisplay *display = SDL_GetDisplay(0);
551
552     if (display) {
553         SDL_DisplayMode *desktopmode = &display->desktop_mode;
554         SDL_DisplayMode *currentmode = &display->current_mode;
555         SDL_DisplayOrientation orientation = SDL_ORIENTATION_UNKNOWN;
556
557         /* The desktop display mode should be kept in sync with the screen
558          * orientation so that updating a window's fullscreen state to
559          * SDL_WINDOW_FULLSCREEN_DESKTOP keeps the window dimensions in the
560          * correct orientation. */
561         if (isLandscape != (desktopmode->w > desktopmode->h)) {
562             int height = desktopmode->w;
563             desktopmode->w = desktopmode->h;
564             desktopmode->h = height;
565         }
566
567         /* Same deal with the current mode + SDL_GetCurrentDisplayMode. */
568         if (isLandscape != (currentmode->w > currentmode->h)) {
569             int height = currentmode->w;
570             currentmode->w = currentmode->h;
571             currentmode->h = height;
572         }
573
574         switch ([UIApplication sharedApplication].statusBarOrientation) {
575         case UIInterfaceOrientationPortrait:
576             orientation = SDL_ORIENTATION_PORTRAIT;
577             break;
578         case UIInterfaceOrientationPortraitUpsideDown:
579             orientation = SDL_ORIENTATION_PORTRAIT_FLIPPED;
580             break;
581         case UIInterfaceOrientationLandscapeLeft:
582             /* Bug: UIInterfaceOrientationLandscapeLeft/Right are reversed - http://openradar.appspot.com/7216046 */
583             orientation = SDL_ORIENTATION_LANDSCAPE_FLIPPED;
584             break;
585         case UIInterfaceOrientationLandscapeRight:
586             /* Bug: UIInterfaceOrientationLandscapeLeft/Right are reversed - http://openradar.appspot.com/7216046 */
587             orientation = SDL_ORIENTATION_LANDSCAPE;
588             break;
589         default:
590             break;
591         }
592         SDL_SendDisplayEvent(display, SDL_DISPLAYEVENT_ORIENTATION, orientation);
593     }
594 }
595 #endif /* !TARGET_OS_TV */
596
597 #endif /* SDL_VIDEO_DRIVER_UIKIT */
598
599 /* vi: set ts=4 sw=4 expandtab: */