// yaze iOS Application // Uses SDL2 and ImGui // // This file implements the iOS-specific entry point and UI management for yaze. // It integrates with the modern Controller API and EditorManager infrastructure. // // Key components: // - AppViewController: Main view controller managing the MTKView and Controller lifecycle // - AppDelegate: iOS app lifecycle management and document picker integration // - Touch gesture handlers: Maps iOS gestures to ImGui input events // // Updated to use: // - Modern Controller::OnEntry/OnLoad/DoRender API // - EditorManager for ROM management (no SharedRom singleton) // - Proper SDL2 initialization for iOS // - Updated ImGui backends (SDL2 renderer, not Metal directly) #import #if TARGET_OS_OSX #import #else #import #endif #import #import #import #include #include #include #include "app/controller.h" #include "app/application.h" #include "app/platform/app_delegate.h" #include "app/platform/font_loader.h" #include "app/platform/ios/ios_host.h" #include "app/platform/window.h" #include "rom/rom.h" #include "app/platform/sdl_compat.h" #ifdef main #undef main #endif #include "app/platform/view_controller.h" #include "imgui/backends/imgui_impl_sdl2.h" #include "imgui/backends/imgui_impl_sdlrenderer2.h" #include "imgui/imgui.h" namespace { yaze::ios::IOSHost g_ios_host; } // namespace // ---------------------------------------------------------------------------- // AppViewController // ---------------------------------------------------------------------------- @implementation AppViewController { yaze::AppConfig app_config_; bool host_initialized_; UITouch *primary_touch_; } - (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; _device = MTLCreateSystemDefaultDevice(); _commandQueue = [_device newCommandQueue]; if (!self.device) { NSLog(@"Metal is not supported"); abort(); } // Initialize SDL for iOS SDL_SetMainReady(); #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR SDL_SetHint(SDL_HINT_AUDIO_CATEGORY, "ambient"); if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_TIMER) != 0) { NSLog(@"SDL_Init failed: %s", SDL_GetError()); } #endif #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR SDL_iOSSetEventPump(SDL_TRUE); #endif // Parse command line arguments int argc = NSProcessInfo.processInfo.arguments.count; char **argv = new char *[argc]; for (int i = 0; i < argc; i++) { NSString *arg = NSProcessInfo.processInfo.arguments[i]; const char *cString = [arg UTF8String]; argv[i] = new char[strlen(cString) + 1]; strcpy(argv[i], cString); } std::string rom_filename = ""; if (argc > 1) { rom_filename = argv[1]; } // Clean up argv for (int i = 0; i < argc; i++) { delete[] argv[i]; } delete[] argv; #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR SDL_iOSSetEventPump(SDL_FALSE); #endif // Enable native IME SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1"); // Initialize Application singleton yaze::AppConfig config; config.rom_file = rom_filename; app_config_ = config; host_initialized_ = false; // Setup gesture recognizers _hoverGestureRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(HoverGesture:)]; _hoverGestureRecognizer.cancelsTouchesInView = NO; _hoverGestureRecognizer.delegate = self; [self.view addGestureRecognizer:_hoverGestureRecognizer]; _pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(HandlePinch:)]; _pinchRecognizer.cancelsTouchesInView = NO; _pinchRecognizer.delegate = self; [self.view addGestureRecognizer:_pinchRecognizer]; _longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)]; _longPressRecognizer.cancelsTouchesInView = NO; _longPressRecognizer.delegate = self; [self.view addGestureRecognizer:_longPressRecognizer]; _swipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(HandleSwipe:)]; _swipeRecognizer.direction = UISwipeGestureRecognizerDirectionRight | UISwipeGestureRecognizerDirectionLeft; _swipeRecognizer.cancelsTouchesInView = NO; _swipeRecognizer.delegate = self; [self.view addGestureRecognizer:_swipeRecognizer]; return self; } - (MTKView *)mtkView { return (MTKView *)self.view; } - (void)loadView { self.view = [[MTKView alloc] initWithFrame:CGRectMake(0, 0, 1200, 720)]; } - (void)viewDidLoad { [super viewDidLoad]; self.view.multipleTouchEnabled = YES; self.mtkView.device = self.device; self.mtkView.delegate = self; if (!host_initialized_) { g_ios_host.SetMetalView((__bridge void *)self.view); yaze::ios::IOSHostConfig host_config; host_config.app_config = app_config_; auto status = g_ios_host.Initialize(host_config); if (!status.ok()) { NSLog(@"Failed to initialize iOS host: %s", std::string(status.message()).c_str()); abort(); } self.controller = yaze::Application::Instance().GetController(); if (!self.controller) { NSLog(@"Failed to initialize application controller"); abort(); } host_initialized_ = true; } } - (void)drawInMTKView:(MTKView *)view { auto& app = yaze::Application::Instance(); if (!host_initialized_ || !app.IsReady() || !app.GetController()->IsActive()) { return; } // Update ImGui display size for iOS before Tick // Note: Tick() calls OnInput() then OnLoad() (NewFrame) then DoRender() // We want to update IO before NewFrame. // OnInput handles SDL events. ImGuiIO &io = ImGui::GetIO(); io.DisplaySize.x = view.bounds.size.width; io.DisplaySize.y = view.bounds.size.height; CGFloat framebufferScale = view.window.screen.scale ?: UIScreen.mainScreen.scale; io.DisplayFramebufferScale = ImVec2(framebufferScale, framebufferScale); g_ios_host.Tick(); } - (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size { } // ---------------------------------------------------------------------------- // Input processing // ---------------------------------------------------------------------------- #if !TARGET_OS_OSX // This touch mapping is super cheesy/hacky. We treat any touch on the screen // as if it were a depressed left mouse button, and we don't bother handling // multitouch correctly at all. This causes the "cursor" to behave very erratically // when there are multiple active touches. But for demo purposes, single-touch // interaction actually works surprisingly well. - (void)UpdateIOWithTouchEvent:(UIEvent *)event { ImGuiIO &io = ImGui::GetIO(); io.AddMouseSourceEvent(ImGuiMouseSource_TouchScreen); UITouch *active_touch = nil; if (primary_touch_ && [event.allTouches containsObject:primary_touch_]) { if (primary_touch_.phase != UITouchPhaseEnded && primary_touch_.phase != UITouchPhaseCancelled) { active_touch = primary_touch_; } } if (!active_touch) { for (UITouch *touch in event.allTouches) { if (touch.phase != UITouchPhaseEnded && touch.phase != UITouchPhaseCancelled) { active_touch = touch; break; } } } if (active_touch) { primary_touch_ = active_touch; CGPoint touchLocation = [active_touch locationInView:self.view]; io.AddMousePosEvent(touchLocation.x, touchLocation.y); io.AddMouseButtonEvent(0, true); } else { primary_touch_ = nil; io.AddMouseButtonEvent(0, false); } } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [self UpdateIOWithTouchEvent:event]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [self UpdateIOWithTouchEvent:event]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self UpdateIOWithTouchEvent:event]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [self UpdateIOWithTouchEvent:event]; } - (void)HoverGesture:(UIHoverGestureRecognizer *)gesture { ImGuiIO &io = ImGui::GetIO(); io.AddMouseSourceEvent(ImGuiMouseSource_TouchScreen); // Cast to UIGestureRecognizer to UIGestureRecognizer to get locationInView UIGestureRecognizer *gestureRecognizer = (UIGestureRecognizer *)gesture; if (gesture.zOffset < 0.50) { io.AddMousePosEvent([gestureRecognizer locationInView:self.view].x, [gestureRecognizer locationInView:self.view].y); } } - (void)HandlePinch:(UIPinchGestureRecognizer *)gesture { ImGuiIO &io = ImGui::GetIO(); io.AddMouseSourceEvent(ImGuiMouseSource_TouchScreen); io.AddMouseWheelEvent(0.0f, gesture.scale); UIGestureRecognizer *gestureRecognizer = (UIGestureRecognizer *)gesture; io.AddMousePosEvent([gestureRecognizer locationInView:self.view].x, [gestureRecognizer locationInView:self.view].y); } - (void)HandleSwipe:(UISwipeGestureRecognizer *)gesture { ImGuiIO &io = ImGui::GetIO(); io.AddMouseSourceEvent(ImGuiMouseSource_TouchScreen); if (gesture.direction == UISwipeGestureRecognizerDirectionRight) { io.AddMouseWheelEvent(1.0f, 0.0f); // Swipe Right } else if (gesture.direction == UISwipeGestureRecognizerDirectionLeft) { io.AddMouseWheelEvent(-1.0f, 0.0f); // Swipe Left } UIGestureRecognizer *gestureRecognizer = (UIGestureRecognizer *)gesture; io.AddMousePosEvent([gestureRecognizer locationInView:self.view].x, [gestureRecognizer locationInView:self.view].y); } - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture { ImGuiIO &io = ImGui::GetIO(); io.AddMouseSourceEvent(ImGuiMouseSource_TouchScreen); io.AddMouseButtonEvent(1, gesture.state == UIGestureRecognizerStateBegan); UIGestureRecognizer *gestureRecognizer = (UIGestureRecognizer *)gesture; io.AddMousePosEvent([gestureRecognizer locationInView:self.view].x, [gestureRecognizer locationInView:self.view].y); } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer: (UIGestureRecognizer *)otherGestureRecognizer { (void)gestureRecognizer; (void)otherGestureRecognizer; return YES; } #endif @end // ---------------------------------------------------------------------------- // SceneDelegate (UIScene lifecycle) // ---------------------------------------------------------------------------- #if !TARGET_OS_OSX @interface SceneDelegate : UIResponder @property(nonatomic, strong) UIWindow *window; @end @implementation SceneDelegate - (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { if (![scene isKindOfClass:[UIWindowScene class]]) { return; } UIWindowScene *windowScene = (UIWindowScene *)scene; UIViewController *rootViewController = [[AppViewController alloc] init]; self.window = [[UIWindow alloc] initWithWindowScene:windowScene]; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; } @end #endif // ---------------------------------------------------------------------------- // AppDelegate // ---------------------------------------------------------------------------- #if TARGET_OS_OSX @interface AppDelegate : NSObject @property(nonatomic, strong) NSWindow *window; @end @implementation AppDelegate - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { return YES; } - (instancetype)init { if (self = [super init]) { NSViewController *rootViewController = [[AppViewController alloc] initWithNibName:nil bundle:nil]; self.window = [[NSWindow alloc] initWithContentRect:NSZeroRect styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable backing:NSBackingStoreBuffered defer:NO]; self.window.contentViewController = rootViewController; [self.window center]; [self.window makeKeyAndOrderFront:self]; } return self; } @end #else @interface AppDelegate : UIResponder @property(nonatomic, strong) UIWindow *window; @property(nonatomic, copy) void (^completionHandler)(NSString *selectedFile); @property(nonatomic, strong) UIDocumentPickerViewController *documentPicker; @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { if (@available(iOS 13.0, *)) { return YES; } UIViewController *rootViewController = [[AppViewController alloc] init]; self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; self.window.rootViewController = rootViewController; [self.window makeKeyAndVisible]; return YES; } - (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { if (@available(iOS 13.0, *)) { UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; configuration.delegateClass = [SceneDelegate class]; configuration.sceneClass = [UIWindowScene class]; return configuration; } return nil; } - (void)applicationWillTerminate:(UIApplication *)application { g_ios_host.Shutdown(); } - (UIViewController *)RootViewControllerForPresenting { if (@available(iOS 13.0, *)) { for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) { if (scene.activationState != UISceneActivationStateForegroundActive) { continue; } if (![scene isKindOfClass:[UIWindowScene class]]) { continue; } UIWindowScene *windowScene = (UIWindowScene *)scene; for (UIWindow *window in windowScene.windows) { if (window.isKeyWindow && window.rootViewController) { return window.rootViewController; } } if (windowScene.windows.count > 0 && windowScene.windows.firstObject.rootViewController) { return windowScene.windows.firstObject.rootViewController; } } } return self.window.rootViewController; } - (void)PresentDocumentPickerWithCompletionHandler: (void (^)(NSString *selectedFile))completionHandler allowedTypes:(NSArray *)allowedTypes { self.completionHandler = completionHandler; NSArray* documentTypes = allowedTypes; if (!documentTypes || documentTypes.count == 0) { documentTypes = @[ UTTypeData ]; } UIViewController *rootViewController = [self RootViewControllerForPresenting]; if (!rootViewController) { if (self.completionHandler) { self.completionHandler(@""); } self.completionHandler = nil; return; } _documentPicker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:documentTypes]; _documentPicker.delegate = self; _documentPicker.modalPresentationStyle = UIModalPresentationFormSheet; [rootViewController presentViewController:_documentPicker animated:YES completion:nil]; } - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls { NSURL *selectedFileURL = [urls firstObject]; if (self.completionHandler) { if (selectedFileURL) { // Create a temporary file path NSString *tempDir = NSTemporaryDirectory(); NSString *fileName = [selectedFileURL lastPathComponent]; NSString *tempPath = [tempDir stringByAppendingPathComponent:fileName]; NSURL *tempURL = [NSURL fileURLWithPath:tempPath]; // Copy the file to the temporary location NSError *error = nil; [[NSFileManager defaultManager] removeItemAtURL:tempURL error:nil]; // Remove if exists [selectedFileURL startAccessingSecurityScopedResource]; BOOL success = [[NSFileManager defaultManager] copyItemAtURL:selectedFileURL toURL:tempURL error:&error]; [selectedFileURL stopAccessingSecurityScopedResource]; if (success) { std::string cppPath = std::string([tempPath UTF8String]); NSLog(@"File copied to temporary path: %s", cppPath.c_str()); self.completionHandler(tempPath); } else { NSLog(@"Failed to copy ROM to temp directory: %@", error); self.completionHandler(@""); } } else { self.completionHandler(@""); } } self.completionHandler = nil; [controller dismissViewControllerAnimated:YES completion:nil]; } - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { if (self.completionHandler) { self.completionHandler(@""); } self.completionHandler = nil; [controller dismissViewControllerAnimated:YES completion:nil]; } @end #endif // ---------------------------------------------------------------------------- // Application main() function // ---------------------------------------------------------------------------- #if TARGET_OS_OSX int main(int argc, const char *argv[]) { return NSApplicationMain(argc, argv); } #else int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } #endif