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