SDL_uikitmodes.m (18141B)
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: */