git.y1.nz

SameBoy

Accurate GB/GBC emulator
download: https://git.y1.nz/archives/sameboy.tar.gz
README | Files | Log | Refs | LICENSE

iOS/GBBackgroundView.m

      1 #import "GBBackgroundView.h"
      2 #import "GBViewMetal.h"
      3 #import "GBHapticManager.h"
      4 #import "GBMenuViewController.h"
      5 #import "GBViewController.h"
      6 #import "GBROMManager.h"
      7 
      8 static double CGPointSquaredDistance(CGPoint a, CGPoint b)
      9 {
     10     double deltaX = a.x - b.x;
     11     double deltaY = a.y - b.y;
     12     return deltaX * deltaX + deltaY * deltaY;
     13 }
     14 
     15 static double CGPointAngle(CGPoint a, CGPoint b)
     16 {
     17     double deltaX = a.x - b.x;
     18     double deltaY = a.y - b.y;
     19     return atan2(deltaY, deltaX);
     20 }
     21 
     22 static void positionView(UIImageView *view, CGPoint position)
     23 {
     24     double center = view.image.size.width / 2 * [UIScreen mainScreen].scale;
     25     view.frame = (CGRect){
     26         {
     27             round(position.x - center) / [UIScreen mainScreen].scale,
     28             round(position.y - center) / [UIScreen mainScreen].scale
     29         },
     30         view.image.size
     31     };
     32 }
     33 
     34 static GB_key_mask_t angleToKeyMask(double angle)
     35 {
     36     signed quantizedAngle = round(angle / M_PI * 16);
     37     if (quantizedAngle < 0) {
     38         quantizedAngle += 32;
     39     }
     40     switch (quantizedAngle) {
     41         case 32:
     42         case  0: return GB_KEY_RIGHT_MASK;
     43         case  1: return GB_KEY_RIGHT_MASK;
     44         case  2: return GB_KEY_RIGHT_MASK;
     45         case  3: return GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK;
     46         case  4: return GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK;
     47         case  5: return GB_KEY_DOWN_MASK;
     48         case  6: return GB_KEY_DOWN_MASK;
     49         case  7: return GB_KEY_DOWN_MASK;
     50             
     51         case  8: return GB_KEY_DOWN_MASK;
     52         case  9: return GB_KEY_DOWN_MASK;
     53         case 10: return GB_KEY_DOWN_MASK;
     54         case 11: return GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK;
     55         case 12: return GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK;
     56         case 13: return GB_KEY_LEFT_MASK;
     57         case 14: return GB_KEY_LEFT_MASK;
     58         case 15: return GB_KEY_LEFT_MASK;
     59             
     60         case 16: return GB_KEY_LEFT_MASK;
     61         case 17: return GB_KEY_LEFT_MASK;
     62         case 18: return GB_KEY_LEFT_MASK;
     63         case 19: return GB_KEY_LEFT_MASK | GB_KEY_UP_MASK;
     64         case 20: return GB_KEY_LEFT_MASK | GB_KEY_UP_MASK;
     65         case 21: return GB_KEY_UP_MASK;
     66         case 22: return GB_KEY_UP_MASK;
     67         case 23: return GB_KEY_UP_MASK;
     68             
     69         case 24: return GB_KEY_UP_MASK;
     70         case 25: return GB_KEY_UP_MASK;
     71         case 26: return GB_KEY_UP_MASK;
     72         case 27: return GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK;
     73         case 28: return GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK;
     74         case 29: return GB_KEY_RIGHT_MASK;
     75         case 30: return GB_KEY_RIGHT_MASK;
     76         case 31: return GB_KEY_RIGHT_MASK;
     77     }
     78     
     79     return 0;
     80 }
     81 
     82 @implementation GBBackgroundView
     83 {
     84     NSMutableSet<UITouch *> *_touches;
     85     UITouch *_padTouch;
     86     CGPoint _padSwipeOrigin;
     87     UITouch *_screenTouch;
     88     UITouch *_logoTouch;
     89     CGPoint _screenSwipeOrigin;
     90     bool _screenSwiped;
     91     bool _inDynamicSpeedMode;
     92     bool _previewMode;
     93     
     94     UIView *_fadeView;
     95     UIImageView *_dpadView;
     96     UIImageView *_dpadShadowView;
     97     UIImageView *_aButtonView;
     98     UIImageView *_bButtonView;
     99     UIImageView *_startButtonView;
    100     UIImageView *_selectButtonView;
    101     UILabel *_screenLabel;
    102     
    103     UIVisualEffectView *_overlayView;
    104     UIView *_overlayViewContents;
    105     NSTimer *_fadeTimer;
    106     
    107     GB_key_mask_t _lastMask;
    108     GBControllerFocus _fullScreenMode;
    109 }
    110 
    111 - (void)reloadThemeImages
    112 {
    113     _aButtonView.image      = [_layout.theme imageNamed:@"buttonA"];
    114     _bButtonView.image      = [_layout.theme imageNamed:@"buttonB"];
    115     _startButtonView.image  = [_layout.theme imageNamed:@"button2"];
    116     _selectButtonView.image = [_layout.theme imageNamed:@"button2"];
    117     self.usesSwipePad = self.usesSwipePad;
    118 }
    119 
    120 - (void)setDefaultScreenLabel
    121 {
    122     _screenLabel.text = @"Tap the Game Boy screen to open the menu and load a ROM from the library.";
    123 }
    124 
    125 
    126 - (instancetype)initWithLayout:(GBLayout *)layout;
    127 {
    128     self = [super initWithImage:nil];
    129     if (!self) return nil;
    130     
    131     _layout = layout;
    132     _touches = [NSMutableSet set];
    133     
    134     _screenLabel = [[UILabel alloc] initWithFrame:CGRectZero];
    135     _screenLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightMedium];
    136     _screenLabel.textAlignment = NSTextAlignmentCenter;
    137     _screenLabel.textColor = [UIColor whiteColor];
    138     _screenLabel.lineBreakMode = NSLineBreakByWordWrapping;
    139     _screenLabel.numberOfLines = 0;
    140     [self setDefaultScreenLabel];
    141     [self addSubview:_screenLabel];
    142     
    143     _dpadView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"dpad"]];
    144     _aButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"buttonA"]];
    145     _bButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"buttonB"]];
    146     _startButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"button2"]];
    147     _selectButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"button2"]];
    148     
    149     _dpadShadowView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"dpadShadow"]];
    150     _dpadShadowView.hidden = true;
    151     _gbView = [[GBViewMetal alloc] initWithFrame:CGRectZero];
    152     
    153     _fadeView = [[UIView alloc] initWithFrame:self.frame];
    154     _fadeView.backgroundColor = [UIColor colorWithWhite:0 alpha:0];
    155     _fadeView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    156     _fadeView.multipleTouchEnabled = true;
    157     
    158     [self addSubview:_dpadView];
    159     [self addSubview:_aButtonView];
    160     [self addSubview:_bButtonView];
    161     [self addSubview:_startButtonView];
    162     [self addSubview:_selectButtonView];
    163     [self addSubview:_fadeView];
    164     [self addSubview:_gbView];
    165     
    166     [_dpadView addSubview:_dpadShadowView];
    167     
    168     UIVisualEffect *effect = [UIBlurEffect effectWithStyle:(UIBlurEffectStyle)UIBlurEffectStyleDark];
    169     _overlayView = [[UIVisualEffectView alloc] initWithEffect:effect];
    170     _overlayView.frame = CGRectMake(8, 8, 32, 32);
    171     _overlayView.layer.cornerRadius = 12;
    172     _overlayView.layer.masksToBounds = true;
    173     _overlayView.alpha = 0;
    174     
    175     if (@available(iOS 13.0, *)) {
    176         _overlayViewContents = [[UIImageView alloc] init];
    177         _overlayViewContents.tintColor = [UIColor whiteColor];
    178         _overlayViewContents.contentMode = UIViewContentModeCenter;
    179     }
    180     else {
    181         _overlayViewContents = [[UILabel alloc] init];
    182         ((UILabel *)_overlayViewContents).font = [UIFont systemFontOfSize:UIFont.systemFontSize weight:UIFontWeightMedium];
    183         ((UILabel *)_overlayViewContents).textColor = [UIColor whiteColor];
    184     }
    185     _overlayViewContents.frame = CGRectMake(8, 8, 160, 20.5);
    186     [_overlayView.contentView addSubview:_overlayViewContents];
    187     [_gbView addSubview:_overlayView];
    188     
    189     return self;
    190 }
    191 
    192 - (GBViewController *)viewController
    193 {
    194     return (GBViewController *)[UIApplication sharedApplication].delegate;
    195 }
    196 
    197 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    198 {
    199     if (_previewMode) return;
    200     if (_fullScreenMode) {
    201         self.fullScreenMode = GBControllerFocusOff;
    202         return;
    203     }
    204     static const double dpadRadius = 75;
    205     CGPoint dpadLocation = _layout.dpadLocation;
    206     double factor = [UIScreen mainScreen].scale;
    207     dpadLocation.x /= factor;
    208     dpadLocation.y /= factor;
    209     CGRect logoRect = _layout.logoRect;
    210     
    211     logoRect.origin.x /= factor;
    212     logoRect.origin.y /= factor;
    213     logoRect.size.width /= factor;
    214     logoRect.size.height /= factor;
    215     
    216     for (UITouch *touch in touches) {
    217         CGPoint point = [touch locationInView:self];
    218         if (CGRectContainsPoint(self.gbView.frame, point) && !_screenTouch) {
    219             if (self.viewController.runMode != GBRunModeNormal) {
    220                 self.viewController.runMode = GBRunModeNormal;
    221                 [self fadeOverlayOut];
    222             }
    223             else {
    224                 _screenTouch = touch;
    225                 _screenSwipeOrigin = point;
    226                 _screenSwiped = false;
    227                 _inDynamicSpeedMode = false;
    228                 _overlayView.alpha = 0;
    229                 [_fadeTimer invalidate];
    230                 _fadeTimer = nil;
    231                 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
    232                     self.viewController.runMode = GBRunModePaused;
    233                     [self displayOverlayWithImage:@"pause" orTitle:@"Paused"];
    234                 }
    235             }
    236         }
    237         else if (CGRectContainsPoint(logoRect, point) && !_logoTouch) {
    238             _logoTouch = touch;
    239         }
    240         else if (!_padTouch) {
    241             if (fabs(point.x - dpadLocation.x) <= dpadRadius &&
    242                 fabs(point.y - dpadLocation.y) <= dpadRadius) {
    243                 _padTouch = touch;
    244                 _padSwipeOrigin = point;
    245             }
    246         }
    247     }
    248     [_touches unionSet:touches];
    249     [self touchesChanged];
    250 }
    251 
    252 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    253 {
    254     if (_padTouch && [touches containsObject:_padTouch]) {
    255         _padTouch = nil;
    256     }
    257     
    258     if (_screenTouch && [touches containsObject:_screenTouch]) {
    259         _screenTouch = nil;
    260         if (self.viewController.runMode == GBRunModePaused) {
    261             self.viewController.runMode = GBRunModeNormal;
    262             [self fadeOverlayOut];
    263         }
    264         if (!_screenSwiped) {
    265             self.window.backgroundColor = nil;
    266             [self.window.rootViewController presentViewController:[GBMenuViewController menu] animated:true completion:nil];
    267         }
    268         if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeLock"]) {
    269             if (self.viewController.runMode != GBRunModeNormal) {
    270                 self.viewController.runMode = GBRunModeNormal;
    271                 [self fadeOverlayOut];
    272             }
    273         }
    274     }
    275     
    276     if (_logoTouch && [touches containsObject:_logoTouch]) {
    277         
    278         double factor = [UIScreen mainScreen].scale;
    279         CGRect logoRect = _layout.logoRect;
    280         
    281         logoRect.origin.x /= factor;
    282         logoRect.origin.y /= factor;
    283         logoRect.size.width /= factor;
    284         logoRect.size.height /= factor;
    285         
    286         CGPoint point = [_logoTouch locationInView:self];
    287         if (CGRectContainsPoint(logoRect, point)) {
    288             self.window.backgroundColor = nil;
    289             [self.window.rootViewController presentViewController:[GBMenuViewController menu] animated:true completion:nil];
    290         }
    291         _logoTouch = nil;
    292     }
    293 
    294     [_touches minusSet:touches];
    295     [self touchesChanged];
    296 }
    297 
    298 - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    299 {
    300     [self touchesEnded:touches withEvent:event];
    301 }
    302 
    303 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    304 {
    305     [self touchesChanged];
    306 }
    307 
    308 - (void)touchesChanged
    309 {
    310     if (_previewMode) return;
    311     if (!GB_is_inited(_gbView.gb)) return;
    312     GB_key_mask_t mask = 0;
    313     double factor = [UIScreen mainScreen].scale;
    314     double buttonRadiusSquared = 36 *  36 * factor * factor;
    315     double dpadRadius = 75 * factor;
    316     bool dpadHandled = false;
    317     if (_usesSwipePad) {
    318         dpadHandled = true;
    319         if (_padTouch) {
    320             CGPoint point = [_padTouch locationInView:self];
    321             double squaredDistance = CGPointSquaredDistance(point, _padSwipeOrigin);
    322             if (squaredDistance > 16 * 16) {
    323                 GB_set_use_faux_analog_inputs(_gbView.gb, 0, false);
    324                 double angle = CGPointAngle(point, _padSwipeOrigin);
    325                 mask |= angleToKeyMask(angle);
    326                 if (squaredDistance > 24 * 24) {
    327                     double deltaX = point.x - _padSwipeOrigin.x;
    328                     double deltaY = point.y - _padSwipeOrigin.y;
    329                     double distance = sqrt(squaredDistance);
    330                     _padSwipeOrigin.x = point.x - deltaX / distance * 24;
    331                     _padSwipeOrigin.y = point.y - deltaY / distance * 24;
    332                 }
    333             }
    334         }
    335     }
    336     for (UITouch *touch in _touches) {
    337         if (_usesSwipePad && touch == _padTouch) continue;
    338         CGPoint point = [touch locationInView:self];
    339         
    340         if (touch == _screenTouch) {
    341             if (_inDynamicSpeedMode) {
    342                 double delta = point.x - _screenSwipeOrigin.x;
    343                 if (fabs(delta) < 32) {
    344                     self.viewController.runMode = GBRunModePaused;
    345                     [self displayOverlayWithImage:@"pause" orTitle:@"Paused"];
    346                     continue;
    347                 }
    348                 
    349                 double speed = fabs(delta / _gbView.frame.size.width * 3);
    350                 if (delta > 0) {
    351                     if (speed > 1) {
    352                         [self displayOverlayWithImage:@"forward" orTitle:@"Fast-forwarding…"];
    353                     }
    354                     else {
    355                         [self displayOverlayWithImage:@"play" orTitle:@"Forward…"];
    356                     }
    357                     GB_set_clock_multiplier(_gbView.gb, speed);
    358                     self.viewController.runMode = GBRunModeTurbo;
    359                 }
    360                 else {
    361                     [self displayOverlayWithImage:@"backward" orTitle:@"Rewinding…"];
    362                     GB_set_clock_multiplier(_gbView.gb, speed);
    363                     self.viewController.runMode = GBRunModeRewind;
    364 
    365                 }
    366                 continue;
    367             }
    368             if (_screenSwiped) continue;
    369             if (point.x - _screenSwipeOrigin.x > 32) {
    370                 [self turboSwipe];
    371             }
    372             else if (point.x - _screenSwipeOrigin.x < -32) {
    373                 [self rewindSwipe];
    374             }
    375             else if (point.y - _screenSwipeOrigin.y > 32) {
    376                 [self saveSwipeFromController:false];
    377             }
    378             else if (point.y - _screenSwipeOrigin.y < -32) {
    379                 [self loadSwipeFromController:false];
    380             }
    381             continue;
    382         }
    383         
    384         point.x *= factor;
    385         point.y *= factor;
    386         if (!dpadHandled &&
    387             (touch == _padTouch ||
    388                 (fabs(point.x - _layout.dpadLocation.x) <= dpadRadius &&
    389                  fabs(point.y - _layout.dpadLocation.y) <= dpadRadius)
    390             ) && (fabs(point.x - _layout.dpadLocation.x) >= dpadRadius / 5 ||
    391                   fabs(point.y - _layout.dpadLocation.y) >= dpadRadius / 5)) {
    392             GB_set_use_faux_analog_inputs(_gbView.gb, 0, false);
    393             dpadHandled = true; // Don't handle the dpad twice
    394             double angle = CGPointAngle(point, _layout.dpadLocation);
    395             mask |= angleToKeyMask(angle);
    396         }
    397         else if (CGPointSquaredDistance(point, _layout.aLocation) <= buttonRadiusSquared) {
    398             mask |= GB_KEY_A_MASK;
    399         }
    400         else if (CGPointSquaredDistance(point, _layout.bLocation) <= buttonRadiusSquared) {
    401             mask |= GB_KEY_B_MASK;
    402         }
    403         else if (CGPointSquaredDistance(point, _layout.startLocation) <= buttonRadiusSquared) {
    404             mask |= GB_KEY_START_MASK;
    405         }
    406         else if (CGPointSquaredDistance(point, _layout.selectLocation) <= buttonRadiusSquared) {
    407             mask |= GB_KEY_SELECT_MASK;
    408         }
    409         else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBEnableABCombo"] &&
    410                  CGPointSquaredDistance(point, _layout.abComboLocation) <= buttonRadiusSquared) {
    411             mask |= GB_KEY_A_MASK | GB_KEY_B_MASK;
    412         }
    413     }
    414     if (mask != _lastMask) {
    415         _aButtonView.image      = [_layout.theme imageNamed:(mask & GB_KEY_A_MASK)?      @"buttonAPressed" : @"buttonA"];
    416         _bButtonView.image      = [_layout.theme imageNamed:(mask & GB_KEY_B_MASK)?      @"buttonBPressed" : @"buttonB"];
    417         _startButtonView.image  = [_layout.theme imageNamed:(mask & GB_KEY_START_MASK) ? @"button2Pressed" : @"button2"];
    418         _selectButtonView.image = [_layout.theme imageNamed:(mask & GB_KEY_SELECT_MASK)? @"button2Pressed" : @"button2"];
    419         
    420         bool hidden = false;
    421         bool diagonal = false;
    422         double rotation = 0;
    423         switch (mask & (GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK | GB_KEY_LEFT_MASK | GB_KEY_UP_MASK)) {
    424             case GB_KEY_RIGHT_MASK: break;
    425             case GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK: diagonal = true; break;
    426             case GB_KEY_DOWN_MASK: rotation = M_PI_2; break;
    427             case GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK: diagonal = true; rotation = M_PI_2; break;
    428             case GB_KEY_LEFT_MASK: rotation = M_PI; break;
    429             case GB_KEY_LEFT_MASK | GB_KEY_UP_MASK: diagonal = true; rotation = M_PI; break;
    430             case GB_KEY_UP_MASK: rotation = -M_PI_2; break;
    431             case GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK: diagonal = true; rotation = -M_PI_2; break;
    432             default:
    433                 hidden = true;
    434         }
    435         
    436         _dpadShadowView.hidden = hidden;
    437         if (!hidden) {
    438             if (_usesSwipePad) {
    439                 _dpadShadowView.image = [UIImage imageNamed:diagonal? @"swipepadShadowDiagonal" : @"swipepadShadow"];
    440 
    441             }
    442             else {
    443                 _dpadShadowView.image = [UIImage imageNamed:diagonal? @"dpadShadowDiagonal" : @"dpadShadow"];
    444             }
    445             _dpadShadowView.transform = CGAffineTransformMakeRotation(rotation);
    446         }
    447         
    448         GB_set_key_mask(_gbView.gb, mask);
    449         if ((mask & ~_lastMask) && ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBButtonHaptics"])) {
    450             [[GBHapticManager sharedManager] doTapHaptic];
    451         }
    452         _lastMask = mask;
    453         
    454         GBViewController *viewController = self.viewController;
    455         GBRunMode runMode = viewController.runMode;
    456         if (runMode == GBRunModeRewind || runMode == GBRunModePaused) {
    457             viewController.runMode = GBRunModeNormal;
    458             [self fadeOverlayOut];
    459         }
    460     }
    461 }
    462 
    463 - (BOOL)isMultipleTouchEnabled
    464 {
    465     return true;
    466 }
    467 
    468 - (BOOL)isUserInteractionEnabled
    469 {
    470     return true;
    471 }
    472 
    473 - (void)setLayout:(GBLayout *)layout
    474 {
    475     _layout = layout;
    476     self.image = layout.background;
    477     
    478     positionView(_dpadView, layout.dpadLocation);
    479     positionView(_aButtonView, layout.aLocation);
    480     positionView(_bButtonView, layout.bLocation);
    481     positionView(_startButtonView, layout.startLocation);
    482     positionView(_selectButtonView, layout.selectLocation);
    483     
    484     CGRect screenFrame = layout.screenRect;
    485     screenFrame.origin.x /= [UIScreen mainScreen].scale;
    486     screenFrame.origin.y /= [UIScreen mainScreen].scale;
    487     screenFrame.size.width /= [UIScreen mainScreen].scale;
    488     screenFrame.size.height /= [UIScreen mainScreen].scale;
    489     
    490     if (_fullScreenMode == GBControllerFocusOn) {
    491         CGRect fullScreenFrame = layout.fullScreenRect;
    492         fullScreenFrame.origin.x /= [UIScreen mainScreen].scale;
    493         fullScreenFrame.origin.y /= [UIScreen mainScreen].scale;
    494         fullScreenFrame.size.width /= [UIScreen mainScreen].scale;
    495         fullScreenFrame.size.height /= [UIScreen mainScreen].scale;
    496         _gbView.frame = fullScreenFrame;
    497     }
    498     else {
    499         _gbView.frame = screenFrame;
    500     }
    501     
    502     screenFrame.origin.x += 8;
    503     screenFrame.origin.y += 8;
    504     screenFrame.size.width -= 16;
    505     screenFrame.size.height -= 16;
    506     
    507     if (@available(iOS 13.0, *)) {
    508         self.overrideUserInterfaceStyle = layout.theme.isDark? UIUserInterfaceStyleDark : UIUserInterfaceStyleLight;
    509         self.tintColor = layout.theme.buttonColor;
    510     }
    511 
    512     _screenLabel.frame = screenFrame;
    513 }
    514 
    515 - (void)setUsesSwipePad:(bool)usesSwipePad
    516 {
    517     _usesSwipePad = usesSwipePad;
    518     _dpadView.image = nil; // Some bug in UIImage seems to trigger without this?
    519     _dpadView.image = [_layout.theme imageNamed:usesSwipePad? @"swipepad" : @"dpad"];
    520 }
    521 
    522 - (void)displayOverlayWithImage:(NSString *)imageName orTitle:(NSString *)title
    523 {
    524     if (@available(iOS 13.0, *)) {
    525         ((UIImageView *)_overlayViewContents).image = [UIImage systemImageNamed:imageName
    526                                                               withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightMedium]];
    527     }
    528     else {
    529         ((UILabel *)_overlayViewContents).text = title;
    530     }
    531     [_overlayViewContents sizeToFit];
    532 
    533     CGRect frame = _overlayViewContents.frame;
    534     frame.size.width = MAX(frame.size.width, 25);
    535     frame.size.height = MAX(frame.size.height, 22);
    536     _overlayViewContents.frame = frame;
    537     frame.origin = (CGPoint){8, 8};
    538     frame.size.width += 16;
    539     frame.size.height += 16;
    540     _overlayView.frame = frame;
    541     
    542     _overlayView.alpha = 1.0;
    543 }
    544 
    545 - (void)fadeOverlayOut
    546 {
    547     [UIView animateWithDuration:1 animations:^{
    548         _overlayView.alpha = 0;
    549     }];
    550     [_fadeTimer invalidate];
    551     _fadeTimer = nil;
    552 }
    553 
    554 - (void)turboSwipe
    555 {
    556     _screenSwiped = true;
    557     if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
    558         _inDynamicSpeedMode = true;
    559     }
    560     [self displayOverlayWithImage:@"forward" orTitle:@"Fast-forwarding…"];
    561     self.viewController.runMode = GBRunModeTurbo;
    562 }
    563 
    564 - (void)rewindSwipe
    565 {
    566     _screenSwiped = true;
    567     if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
    568         _inDynamicSpeedMode = true;
    569     }
    570     [self displayOverlayWithImage:@"backward" orTitle:@"Rewinding…"];
    571     self.viewController.runMode = GBRunModeRewind;
    572 }
    573 
    574 - (NSString *)swipeStateFile
    575 {
    576     return [[GBROMManager sharedManager] stateFile:1];
    577 }
    578 
    579 - (void)saveSwipeFromController:(bool)fromController
    580 {
    581     if (!fromController) {
    582         _screenSwiped = true;
    583         self.viewController.runMode = GBRunModeNormal;
    584         if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeState"]) {
    585             [self fadeOverlayOut];
    586             return;
    587         }
    588     }
    589     [self displayOverlayWithImage:@"square.and.arrow.down" orTitle:@"Saved state to Slot 1"];
    590     _fadeTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:false block:^(NSTimer *timer) {
    591         [self fadeOverlayOut];
    592     }];
    593     [self.viewController stop];
    594     [self.viewController saveStateToFile:self.swipeStateFile];
    595     [self.viewController start];
    596 }
    597 
    598 - (void)loadSwipeFromController:(bool)fromController
    599 {
    600     if (!fromController) {
    601         _screenSwiped = true;
    602         self.viewController.runMode = GBRunModeNormal;
    603         if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeState"]) {
    604             [self fadeOverlayOut];
    605             return;
    606         }
    607     }
    608     [self displayOverlayWithImage:@"square.and.arrow.up" orTitle:@"Loaded state from Slot 1"];
    609     _fadeTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:false block:^(NSTimer *timer) {
    610         [self fadeOverlayOut];
    611     }];
    612     [self.viewController stop];
    613     [self.viewController loadStateFromFile:self.swipeStateFile];
    614     [self.viewController start];
    615 }
    616 
    617 - (void)enterPreviewMode:(bool)showLabel
    618 {
    619     if (showLabel) {
    620         _screenLabel.text = [NSString stringWithFormat:@"Previewing Theme “%@”", _layout.theme.name];
    621     }
    622     else {
    623         [_screenLabel removeFromSuperview];
    624         _screenLabel = nil;
    625     }
    626     _previewMode = true;
    627 }
    628 
    629 - (GBControllerFocus)fullScreenMode
    630 {
    631     return _fullScreenMode;
    632 }
    633 
    634 - (void)setFullScreenMode:(GBControllerFocus)fullScreenMode
    635 {
    636     if (fullScreenMode == _fullScreenMode) return;
    637     _fullScreenMode = fullScreenMode;
    638     [UIView animateWithDuration:1.0/3 animations:^{
    639         // Animating alpha has some weird quirks for some reason
    640         _fadeView.backgroundColor = [UIColor colorWithWhite:0 alpha:fullScreenMode? 1 : 0];
    641         [self setLayout:_layout];
    642     }];
    643     [self.window.rootViewController setNeedsStatusBarAppearanceUpdate];
    644 }
    645 
    646 @end

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.