#import "GBBackgroundView.h" #import "GBViewMetal.h" #import "GBHapticManager.h" #import "GBMenuViewController.h" #import "GBViewController.h" #import "GBROMManager.h" static double CGPointSquaredDistance(CGPoint a, CGPoint b) { double deltaX = a.x - b.x; double deltaY = a.y - b.y; return deltaX * deltaX + deltaY * deltaY; } static double CGPointAngle(CGPoint a, CGPoint b) { double deltaX = a.x - b.x; double deltaY = a.y - b.y; return atan2(deltaY, deltaX); } static void positionView(UIImageView *view, CGPoint position) { double center = view.image.size.width / 2 * [UIScreen mainScreen].scale; view.frame = (CGRect){ { round(position.x - center) / [UIScreen mainScreen].scale, round(position.y - center) / [UIScreen mainScreen].scale }, view.image.size }; } static GB_key_mask_t angleToKeyMask(double angle) { signed quantizedAngle = round(angle / M_PI * 16); if (quantizedAngle < 0) { quantizedAngle += 32; } switch (quantizedAngle) { case 32: case 0: return GB_KEY_RIGHT_MASK; case 1: return GB_KEY_RIGHT_MASK; case 2: return GB_KEY_RIGHT_MASK; case 3: return GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK; case 4: return GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK; case 5: return GB_KEY_DOWN_MASK; case 6: return GB_KEY_DOWN_MASK; case 7: return GB_KEY_DOWN_MASK; case 8: return GB_KEY_DOWN_MASK; case 9: return GB_KEY_DOWN_MASK; case 10: return GB_KEY_DOWN_MASK; case 11: return GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK; case 12: return GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK; case 13: return GB_KEY_LEFT_MASK; case 14: return GB_KEY_LEFT_MASK; case 15: return GB_KEY_LEFT_MASK; case 16: return GB_KEY_LEFT_MASK; case 17: return GB_KEY_LEFT_MASK; case 18: return GB_KEY_LEFT_MASK; case 19: return GB_KEY_LEFT_MASK | GB_KEY_UP_MASK; case 20: return GB_KEY_LEFT_MASK | GB_KEY_UP_MASK; case 21: return GB_KEY_UP_MASK; case 22: return GB_KEY_UP_MASK; case 23: return GB_KEY_UP_MASK; case 24: return GB_KEY_UP_MASK; case 25: return GB_KEY_UP_MASK; case 26: return GB_KEY_UP_MASK; case 27: return GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK; case 28: return GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK; case 29: return GB_KEY_RIGHT_MASK; case 30: return GB_KEY_RIGHT_MASK; case 31: return GB_KEY_RIGHT_MASK; } return 0; } @implementation GBBackgroundView { NSMutableSet *_touches; UITouch *_padTouch; CGPoint _padSwipeOrigin; UITouch *_screenTouch; UITouch *_logoTouch; CGPoint _screenSwipeOrigin; bool _screenSwiped; bool _inDynamicSpeedMode; bool _previewMode; UIView *_fadeView; UIImageView *_dpadView; UIImageView *_dpadShadowView; UIImageView *_aButtonView; UIImageView *_bButtonView; UIImageView *_startButtonView; UIImageView *_selectButtonView; UILabel *_screenLabel; UIVisualEffectView *_overlayView; UIView *_overlayViewContents; NSTimer *_fadeTimer; GB_key_mask_t _lastMask; bool _fullScreenMode; } - (void)reloadThemeImages { _aButtonView.image = [_layout.theme imageNamed:@"buttonA"]; _bButtonView.image = [_layout.theme imageNamed:@"buttonB"]; _startButtonView.image = [_layout.theme imageNamed:@"button2"]; _selectButtonView.image = [_layout.theme imageNamed:@"button2"]; self.usesSwipePad = self.usesSwipePad; } - (void)setDefaultScreenLabel { _screenLabel.text = @"Tap the Game Boy screen to open the menu and load a ROM from the library."; } - (instancetype)initWithLayout:(GBLayout *)layout; { self = [super initWithImage:nil]; if (!self) return nil; _layout = layout; _touches = [NSMutableSet set]; _screenLabel = [[UILabel alloc] initWithFrame:CGRectZero]; _screenLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightMedium]; _screenLabel.textAlignment = NSTextAlignmentCenter; _screenLabel.textColor = [UIColor whiteColor]; _screenLabel.lineBreakMode = NSLineBreakByWordWrapping; _screenLabel.numberOfLines = 0; [self setDefaultScreenLabel]; [self addSubview:_screenLabel]; _dpadView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"dpad"]]; _aButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"buttonA"]]; _bButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"buttonB"]]; _startButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"button2"]]; _selectButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"button2"]]; _dpadShadowView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"dpadShadow"]]; _dpadShadowView.hidden = true; _gbView = [[GBViewMetal alloc] initWithFrame:CGRectZero]; _fadeView = [[UIView alloc] initWithFrame:self.frame]; _fadeView.backgroundColor = [UIColor colorWithWhite:0 alpha:0]; _fadeView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; _fadeView.multipleTouchEnabled = true; [self addSubview:_dpadView]; [self addSubview:_aButtonView]; [self addSubview:_bButtonView]; [self addSubview:_startButtonView]; [self addSubview:_selectButtonView]; [self addSubview:_fadeView]; [self addSubview:_gbView]; [_dpadView addSubview:_dpadShadowView]; UIVisualEffect *effect = [UIBlurEffect effectWithStyle:(UIBlurEffectStyle)UIBlurEffectStyleDark]; _overlayView = [[UIVisualEffectView alloc] initWithEffect:effect]; _overlayView.frame = CGRectMake(8, 8, 32, 32); _overlayView.layer.cornerRadius = 12; _overlayView.layer.masksToBounds = true; _overlayView.alpha = 0; if (@available(iOS 13.0, *)) { _overlayViewContents = [[UIImageView alloc] init]; _overlayViewContents.tintColor = [UIColor whiteColor]; _overlayViewContents.contentMode = UIViewContentModeCenter; } else { _overlayViewContents = [[UILabel alloc] init]; ((UILabel *)_overlayViewContents).font = [UIFont systemFontOfSize:UIFont.systemFontSize weight:UIFontWeightMedium]; ((UILabel *)_overlayViewContents).textColor = [UIColor whiteColor]; } _overlayViewContents.frame = CGRectMake(8, 8, 160, 20.5); [_overlayView.contentView addSubview:_overlayViewContents]; [_gbView addSubview:_overlayView]; return self; } - (GBViewController *)viewController { return (GBViewController *)[UIApplication sharedApplication].delegate; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { if (_previewMode) return; if (_fullScreenMode) { self.fullScreenMode = false; return; } static const double dpadRadius = 75; CGPoint dpadLocation = _layout.dpadLocation; double factor = [UIScreen mainScreen].scale; dpadLocation.x /= factor; dpadLocation.y /= factor; CGRect logoRect = _layout.logoRect; logoRect.origin.x /= factor; logoRect.origin.y /= factor; logoRect.size.width /= factor; logoRect.size.height /= factor; for (UITouch *touch in touches) { CGPoint point = [touch locationInView:self]; if (CGRectContainsPoint(self.gbView.frame, point) && !_screenTouch) { if (self.viewController.runMode != GBRunModeNormal) { self.viewController.runMode = GBRunModeNormal; [self fadeOverlayOut]; } else { _screenTouch = touch; _screenSwipeOrigin = point; _screenSwiped = false; _inDynamicSpeedMode = false; _overlayView.alpha = 0; [_fadeTimer invalidate]; _fadeTimer = nil; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { self.viewController.runMode = GBRunModePaused; [self displayOverlayWithImage:@"pause" orTitle:@"Paused"]; } } } else if (CGRectContainsPoint(logoRect, point) && !_logoTouch) { _logoTouch = touch; } else if (!_padTouch) { if (fabs(point.x - dpadLocation.x) <= dpadRadius && fabs(point.y - dpadLocation.y) <= dpadRadius) { _padTouch = touch; _padSwipeOrigin = point; } } } [_touches unionSet:touches]; [self touchesChanged]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { if (_padTouch && [touches containsObject:_padTouch]) { _padTouch = nil; } if (_screenTouch && [touches containsObject:_screenTouch]) { _screenTouch = nil; if (self.viewController.runMode == GBRunModePaused) { self.viewController.runMode = GBRunModeNormal; [self fadeOverlayOut]; } if (!_screenSwiped) { self.window.backgroundColor = nil; [self.window.rootViewController presentViewController:[GBMenuViewController menu] animated:true completion:nil]; } if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeLock"]) { if (self.viewController.runMode != GBRunModeNormal) { self.viewController.runMode = GBRunModeNormal; [self fadeOverlayOut]; } } } if (_logoTouch && [touches containsObject:_logoTouch]) { double factor = [UIScreen mainScreen].scale; CGRect logoRect = _layout.logoRect; logoRect.origin.x /= factor; logoRect.origin.y /= factor; logoRect.size.width /= factor; logoRect.size.height /= factor; CGPoint point = [_logoTouch locationInView:self]; if (CGRectContainsPoint(logoRect, point)) { self.window.backgroundColor = nil; [self.window.rootViewController presentViewController:[GBMenuViewController menu] animated:true completion:nil]; } _logoTouch = nil; } [_touches minusSet:touches]; [self touchesChanged]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesEnded:touches withEvent:event]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesChanged]; } - (void)touchesChanged { if (_previewMode) return; if (!GB_is_inited(_gbView.gb)) return; GB_key_mask_t mask = 0; double factor = [UIScreen mainScreen].scale; double buttonRadiusSquared = 36 * 36 * factor * factor; double dpadRadius = 75 * factor; bool dpadHandled = false; if (_usesSwipePad) { dpadHandled = true; if (_padTouch) { CGPoint point = [_padTouch locationInView:self]; double squaredDistance = CGPointSquaredDistance(point, _padSwipeOrigin); if (squaredDistance > 16 * 16) { GB_set_use_faux_analog_inputs(_gbView.gb, 0, false); double angle = CGPointAngle(point, _padSwipeOrigin); mask |= angleToKeyMask(angle); if (squaredDistance > 24 * 24) { double deltaX = point.x - _padSwipeOrigin.x; double deltaY = point.y - _padSwipeOrigin.y; double distance = sqrt(squaredDistance); _padSwipeOrigin.x = point.x - deltaX / distance * 24; _padSwipeOrigin.y = point.y - deltaY / distance * 24; } } } } for (UITouch *touch in _touches) { if (_usesSwipePad && touch == _padTouch) continue; CGPoint point = [touch locationInView:self]; if (touch == _screenTouch) { if (_inDynamicSpeedMode) { double delta = point.x - _screenSwipeOrigin.x; if (fabs(delta) < 32) { self.viewController.runMode = GBRunModePaused; [self displayOverlayWithImage:@"pause" orTitle:@"Paused"]; continue; } double speed = fabs(delta / _gbView.frame.size.width * 3); if (delta > 0) { if (speed > 1) { [self displayOverlayWithImage:@"forward" orTitle:@"Fast-forwarding…"]; } else { [self displayOverlayWithImage:@"play" orTitle:@"Forward…"]; } GB_set_clock_multiplier(_gbView.gb, speed); self.viewController.runMode = GBRunModeTurbo; } else { [self displayOverlayWithImage:@"backward" orTitle:@"Rewinding…"]; GB_set_clock_multiplier(_gbView.gb, speed); self.viewController.runMode = GBRunModeRewind; } continue; } if (_screenSwiped) continue; if (point.x - _screenSwipeOrigin.x > 32) { [self turboSwipe]; } else if (point.x - _screenSwipeOrigin.x < -32) { [self rewindSwipe]; } else if (point.y - _screenSwipeOrigin.y > 32) { [self saveSwipe]; } else if (point.y - _screenSwipeOrigin.y < -32) { [self loadSwipe]; } continue; } point.x *= factor; point.y *= factor; if (!dpadHandled && (touch == _padTouch || (fabs(point.x - _layout.dpadLocation.x) <= dpadRadius && fabs(point.y - _layout.dpadLocation.y) <= dpadRadius) ) && (fabs(point.x - _layout.dpadLocation.x) >= dpadRadius / 5 || fabs(point.y - _layout.dpadLocation.y) >= dpadRadius / 5)) { GB_set_use_faux_analog_inputs(_gbView.gb, 0, false); dpadHandled = true; // Don't handle the dpad twice double angle = CGPointAngle(point, _layout.dpadLocation); mask |= angleToKeyMask(angle); } else if (CGPointSquaredDistance(point, _layout.aLocation) <= buttonRadiusSquared) { mask |= GB_KEY_A_MASK; } else if (CGPointSquaredDistance(point, _layout.bLocation) <= buttonRadiusSquared) { mask |= GB_KEY_B_MASK; } else if (CGPointSquaredDistance(point, _layout.startLocation) <= buttonRadiusSquared) { mask |= GB_KEY_START_MASK; } else if (CGPointSquaredDistance(point, _layout.selectLocation) <= buttonRadiusSquared) { mask |= GB_KEY_SELECT_MASK; } else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBEnableABCombo"] && CGPointSquaredDistance(point, _layout.abComboLocation) <= buttonRadiusSquared) { mask |= GB_KEY_A_MASK | GB_KEY_B_MASK; } } if (mask != _lastMask) { _aButtonView.image = [_layout.theme imageNamed:(mask & GB_KEY_A_MASK)? @"buttonAPressed" : @"buttonA"]; _bButtonView.image = [_layout.theme imageNamed:(mask & GB_KEY_B_MASK)? @"buttonBPressed" : @"buttonB"]; _startButtonView.image = [_layout.theme imageNamed:(mask & GB_KEY_START_MASK) ? @"button2Pressed" : @"button2"]; _selectButtonView.image = [_layout.theme imageNamed:(mask & GB_KEY_SELECT_MASK)? @"button2Pressed" : @"button2"]; bool hidden = false; bool diagonal = false; double rotation = 0; switch (mask & (GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK | GB_KEY_LEFT_MASK | GB_KEY_UP_MASK)) { case GB_KEY_RIGHT_MASK: break; case GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK: diagonal = true; break; case GB_KEY_DOWN_MASK: rotation = M_PI_2; break; case GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK: diagonal = true; rotation = M_PI_2; break; case GB_KEY_LEFT_MASK: rotation = M_PI; break; case GB_KEY_LEFT_MASK | GB_KEY_UP_MASK: diagonal = true; rotation = M_PI; break; case GB_KEY_UP_MASK: rotation = -M_PI_2; break; case GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK: diagonal = true; rotation = -M_PI_2; break; default: hidden = true; } _dpadShadowView.hidden = hidden; if (!hidden) { if (_usesSwipePad) { _dpadShadowView.image = [UIImage imageNamed:diagonal? @"swipepadShadowDiagonal" : @"swipepadShadow"]; } else { _dpadShadowView.image = [UIImage imageNamed:diagonal? @"dpadShadowDiagonal" : @"dpadShadow"]; } _dpadShadowView.transform = CGAffineTransformMakeRotation(rotation); } GB_set_key_mask(_gbView.gb, mask); if ((mask & ~_lastMask) && ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBButtonHaptics"])) { [[GBHapticManager sharedManager] doTapHaptic]; } _lastMask = mask; } } - (BOOL)isMultipleTouchEnabled { return true; } - (BOOL)isUserInteractionEnabled { return true; } - (void)setLayout:(GBLayout *)layout { _layout = layout; self.image = layout.background; positionView(_dpadView, layout.dpadLocation); positionView(_aButtonView, layout.aLocation); positionView(_bButtonView, layout.bLocation); positionView(_startButtonView, layout.startLocation); positionView(_selectButtonView, layout.selectLocation); CGRect screenFrame = layout.screenRect; screenFrame.origin.x /= [UIScreen mainScreen].scale; screenFrame.origin.y /= [UIScreen mainScreen].scale; screenFrame.size.width /= [UIScreen mainScreen].scale; screenFrame.size.height /= [UIScreen mainScreen].scale; if (_fullScreenMode) { CGRect fullScreenFrame = layout.fullScreenRect; fullScreenFrame.origin.x /= [UIScreen mainScreen].scale; fullScreenFrame.origin.y /= [UIScreen mainScreen].scale; fullScreenFrame.size.width /= [UIScreen mainScreen].scale; fullScreenFrame.size.height /= [UIScreen mainScreen].scale; _gbView.frame = fullScreenFrame; } else { _gbView.frame = screenFrame; } screenFrame.origin.x += 8; screenFrame.origin.y += 8; screenFrame.size.width -= 16; screenFrame.size.height -= 16; if (@available(iOS 13.0, *)) { self.overrideUserInterfaceStyle = layout.theme.isDark? UIUserInterfaceStyleDark : UIUserInterfaceStyleLight; self.tintColor = layout.theme.buttonColor; } _screenLabel.frame = screenFrame; } - (void)setUsesSwipePad:(bool)usesSwipePad { _usesSwipePad = usesSwipePad; _dpadView.image = nil; // Some bug in UIImage seems to trigger without this? _dpadView.image = [_layout.theme imageNamed:usesSwipePad? @"swipepad" : @"dpad"]; } - (void)displayOverlayWithImage:(NSString *)imageName orTitle:(NSString *)title { if (@available(iOS 13.0, *)) { ((UIImageView *)_overlayViewContents).image = [UIImage systemImageNamed:imageName withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightMedium]]; } else { ((UILabel *)_overlayViewContents).text = title; } [_overlayViewContents sizeToFit]; CGRect frame = _overlayViewContents.frame; frame.size.width = MAX(frame.size.width, 25); frame.size.height = MAX(frame.size.height, 22); _overlayViewContents.frame = frame; frame.origin = (CGPoint){8, 8}; frame.size.width += 16; frame.size.height += 16; _overlayView.frame = frame; _overlayView.alpha = 1.0; } - (void)fadeOverlayOut { [UIView animateWithDuration:1 animations:^{ _overlayView.alpha = 0; }]; [_fadeTimer invalidate]; _fadeTimer = nil; } - (void)turboSwipe { _screenSwiped = true; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { _inDynamicSpeedMode = true; } [self displayOverlayWithImage:@"forward" orTitle:@"Fast-forwarding…"]; self.viewController.runMode = GBRunModeTurbo; } - (void)rewindSwipe { _screenSwiped = true; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) { _inDynamicSpeedMode = true; } [self displayOverlayWithImage:@"backward" orTitle:@"Rewinding…"]; self.viewController.runMode = GBRunModeRewind; } - (NSString *)swipeStateFile { return [[GBROMManager sharedManager] stateFile:1]; } - (void)saveSwipe { _screenSwiped = true; self.viewController.runMode = GBRunModeNormal; if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeState"]) { [self fadeOverlayOut]; return; } [self displayOverlayWithImage:@"square.and.arrow.down" orTitle:@"Saved state to Slot 1"]; _fadeTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:false block:^(NSTimer *timer) { [self fadeOverlayOut]; }]; [self.viewController stop]; [self.viewController saveStateToFile:self.swipeStateFile]; [self.viewController start]; } - (void)loadSwipe { _screenSwiped = true; self.viewController.runMode = GBRunModeNormal; if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeState"]) { [self fadeOverlayOut]; return; } [self displayOverlayWithImage:@"square.and.arrow.up" orTitle:@"Loaded state from Slot 1"]; _fadeTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:false block:^(NSTimer *timer) { [self fadeOverlayOut]; }]; [self.viewController stop]; [self.viewController loadStateFromFile:self.swipeStateFile]; [self.viewController start]; } - (void)enterPreviewMode:(bool)showLabel { if (showLabel) { _screenLabel.text = [NSString stringWithFormat:@"Previewing Theme “%@”", _layout.theme.name]; } else { [_screenLabel removeFromSuperview]; _screenLabel = nil; } _previewMode = true; } - (bool)fullScreenMode { return _fullScreenMode; } - (void)setFullScreenMode:(bool)fullScreenMode { if (fullScreenMode == _fullScreenMode) return; _fullScreenMode = fullScreenMode; [UIView animateWithDuration:1.0/3 animations:^{ // Animating alpha has some weird quirks for some reason _fadeView.backgroundColor = [UIColor colorWithWhite:0 alpha:fullScreenMode]; [self setLayout:_layout]; }]; [self.window.rootViewController setNeedsStatusBarAppearanceUpdate]; } @end