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