yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
main.mm
Go to the documentation of this file.
1// yaze iOS Application
2// Uses SDL2 and ImGui
3//
4// This file implements the iOS-specific entry point and UI management for yaze.
5// It integrates with the modern Controller API and EditorManager infrastructure.
6//
7// Key components:
8// - AppViewController: Main view controller managing the MTKView and Controller lifecycle
9// - AppDelegate: iOS app lifecycle management and document picker integration
10// - Touch gesture handlers: Maps iOS gestures to ImGui input events
11//
12// Updated to use:
13// - Modern Controller::OnEntry/OnLoad/DoRender API
14// - EditorManager for ROM management (no SharedRom singleton)
15// - Proper SDL2 initialization for iOS
16// - Updated ImGui backends (SDL2 renderer, not Metal directly)
17
18#import <Foundation/Foundation.h>
19
20#if TARGET_OS_OSX
21#import <Cocoa/Cocoa.h>
22#else
23#import <UIKit/UIKit.h>
24#endif
25
26#import <Metal/Metal.h>
27#import <MetalKit/MetalKit.h>
28#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
29
30#include <string>
31#include <vector>
32#include <algorithm>
33
34#include "app/controller.h"
35#include "app/application.h"
39#include "app/platform/window.h"
40#include "rom/rom.h"
41
43
44#ifdef main
45#undef main
46#endif
47
49#include "imgui/backends/imgui_impl_sdl2.h"
50#include "imgui/backends/imgui_impl_sdlrenderer2.h"
51#include "imgui/imgui.h"
52
53namespace {
55} // namespace
56
57// ----------------------------------------------------------------------------
58// AppViewController
59// ----------------------------------------------------------------------------
60
61@implementation AppViewController {
62 yaze::AppConfig app_config_;
65}
66
67- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil
68 bundle:(nullable NSBundle *)nibBundleOrNil {
69 self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
70
71 _device = MTLCreateSystemDefaultDevice();
72 _commandQueue = [_device newCommandQueue];
73
74 if (!self.device) {
75 NSLog(@"Metal is not supported");
76 abort();
77 }
78
79 // Initialize SDL for iOS
80 SDL_SetMainReady();
81#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
82 SDL_SetHint(SDL_HINT_AUDIO_CATEGORY, "ambient");
83 if (SDL_Init(SDL_INIT_AUDIO | SDL_INIT_TIMER) != 0) {
84 NSLog(@"SDL_Init failed: %s", SDL_GetError());
85 }
86#endif
87#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
88 SDL_iOSSetEventPump(SDL_TRUE);
89#endif
90
91 // Parse command line arguments
92 int argc = NSProcessInfo.processInfo.arguments.count;
93 char **argv = new char *[argc];
94 for (int i = 0; i < argc; i++) {
95 NSString *arg = NSProcessInfo.processInfo.arguments[i];
96 const char *cString = [arg UTF8String];
97 argv[i] = new char[strlen(cString) + 1];
98 strcpy(argv[i], cString);
99 }
100
101 std::string rom_filename = "";
102 if (argc > 1) {
103 rom_filename = argv[1];
104 }
105
106 // Clean up argv
107 for (int i = 0; i < argc; i++) {
108 delete[] argv[i];
109 }
110 delete[] argv;
111
112#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
113 SDL_iOSSetEventPump(SDL_FALSE);
114#endif
115
116 // Enable native IME
117 SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
118
119 // Initialize Application singleton
120 yaze::AppConfig config;
121 config.rom_file = rom_filename;
122 app_config_ = config;
123 host_initialized_ = false;
124
125 // Setup gesture recognizers
126 _hoverGestureRecognizer =
127 [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(HoverGesture:)];
128 _hoverGestureRecognizer.cancelsTouchesInView = NO;
129 _hoverGestureRecognizer.delegate = self;
130 [self.view addGestureRecognizer:_hoverGestureRecognizer];
131
132 _pinchRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self
133 action:@selector(HandlePinch:)];
134 _pinchRecognizer.cancelsTouchesInView = NO;
135 _pinchRecognizer.delegate = self;
136 [self.view addGestureRecognizer:_pinchRecognizer];
137
138 _longPressRecognizer =
139 [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
140 _longPressRecognizer.cancelsTouchesInView = NO;
141 _longPressRecognizer.delegate = self;
142 [self.view addGestureRecognizer:_longPressRecognizer];
143
144 _swipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self
145 action:@selector(HandleSwipe:)];
146 _swipeRecognizer.direction =
147 UISwipeGestureRecognizerDirectionRight | UISwipeGestureRecognizerDirectionLeft;
148 _swipeRecognizer.cancelsTouchesInView = NO;
149 _swipeRecognizer.delegate = self;
150 [self.view addGestureRecognizer:_swipeRecognizer];
151
152 return self;
153}
154
155- (MTKView *)mtkView {
156 return (MTKView *)self.view;
157}
158
159- (void)loadView {
160 self.view = [[MTKView alloc] initWithFrame:CGRectMake(0, 0, 1200, 720)];
161}
162
163- (void)viewDidLoad {
164 [super viewDidLoad];
165
166 self.view.multipleTouchEnabled = YES;
167
168 self.mtkView.device = self.device;
169 self.mtkView.delegate = self;
170
171 if (!host_initialized_) {
172 g_ios_host.SetMetalView((__bridge void *)self.view);
173 yaze::ios::IOSHostConfig host_config;
174 host_config.app_config = app_config_;
175 auto status = g_ios_host.Initialize(host_config);
176 if (!status.ok()) {
177 NSLog(@"Failed to initialize iOS host: %s",
178 std::string(status.message()).c_str());
179 abort();
180 }
181
182 self.controller = yaze::Application::Instance().GetController();
183 if (!self.controller) {
184 NSLog(@"Failed to initialize application controller");
185 abort();
186 }
187 host_initialized_ = true;
188 }
189}
190
191- (void)drawInMTKView:(MTKView *)view {
192 auto& app = yaze::Application::Instance();
193 if (!host_initialized_ || !app.IsReady() || !app.GetController()->IsActive()) {
194 return;
195 }
196
197 // Update ImGui display size for iOS before Tick
198 // Note: Tick() calls OnInput() then OnLoad() (NewFrame) then DoRender()
199 // We want to update IO before NewFrame.
200 // OnInput handles SDL events.
201
202 ImGuiIO &io = ImGui::GetIO();
203 io.DisplaySize.x = view.bounds.size.width;
204 io.DisplaySize.y = view.bounds.size.height;
205
206 CGFloat framebufferScale = view.window.screen.scale ?: UIScreen.mainScreen.scale;
207 io.DisplayFramebufferScale = ImVec2(framebufferScale, framebufferScale);
208
210}
211
212- (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size {
213}
214
215// ----------------------------------------------------------------------------
216// Input processing
217// ----------------------------------------------------------------------------
218
219#if !TARGET_OS_OSX
220
221// This touch mapping is super cheesy/hacky. We treat any touch on the screen
222// as if it were a depressed left mouse button, and we don't bother handling
223// multitouch correctly at all. This causes the "cursor" to behave very erratically
224// when there are multiple active touches. But for demo purposes, single-touch
225// interaction actually works surprisingly well.
226- (void)UpdateIOWithTouchEvent:(UIEvent *)event {
227 ImGuiIO &io = ImGui::GetIO();
228 io.AddMouseSourceEvent(ImGuiMouseSource_TouchScreen);
229
230 UITouch *active_touch = nil;
231 if (primary_touch_ && [event.allTouches containsObject:primary_touch_]) {
232 if (primary_touch_.phase != UITouchPhaseEnded &&
233 primary_touch_.phase != UITouchPhaseCancelled) {
234 active_touch = primary_touch_;
235 }
236 }
237
238 if (!active_touch) {
239 for (UITouch *touch in event.allTouches) {
240 if (touch.phase != UITouchPhaseEnded &&
241 touch.phase != UITouchPhaseCancelled) {
242 active_touch = touch;
243 break;
244 }
245 }
246 }
247
248 if (active_touch) {
249 primary_touch_ = active_touch;
250 CGPoint touchLocation = [active_touch locationInView:self.view];
251 io.AddMousePosEvent(touchLocation.x, touchLocation.y);
252 io.AddMouseButtonEvent(0, true);
253 } else {
254 primary_touch_ = nil;
255 io.AddMouseButtonEvent(0, false);
256 }
257}
258
259- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
260 [self UpdateIOWithTouchEvent:event];
261}
262- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
263 [self UpdateIOWithTouchEvent:event];
264}
265- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
266 [self UpdateIOWithTouchEvent:event];
267}
268- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
269 [self UpdateIOWithTouchEvent:event];
270}
271
272- (void)HoverGesture:(UIHoverGestureRecognizer *)gesture {
273 ImGuiIO &io = ImGui::GetIO();
274 io.AddMouseSourceEvent(ImGuiMouseSource_TouchScreen);
275 // Cast to UIGestureRecognizer to UIGestureRecognizer to get locationInView
276 UIGestureRecognizer *gestureRecognizer = (UIGestureRecognizer *)gesture;
277 if (gesture.zOffset < 0.50) {
278 io.AddMousePosEvent([gestureRecognizer locationInView:self.view].x,
279 [gestureRecognizer locationInView:self.view].y);
280 }
281}
282
283- (void)HandlePinch:(UIPinchGestureRecognizer *)gesture {
284 ImGuiIO &io = ImGui::GetIO();
285 io.AddMouseSourceEvent(ImGuiMouseSource_TouchScreen);
286 io.AddMouseWheelEvent(0.0f, gesture.scale);
287 UIGestureRecognizer *gestureRecognizer = (UIGestureRecognizer *)gesture;
288 io.AddMousePosEvent([gestureRecognizer locationInView:self.view].x,
289 [gestureRecognizer locationInView:self.view].y);
290}
291
292- (void)HandleSwipe:(UISwipeGestureRecognizer *)gesture {
293 ImGuiIO &io = ImGui::GetIO();
294 io.AddMouseSourceEvent(ImGuiMouseSource_TouchScreen);
295 if (gesture.direction == UISwipeGestureRecognizerDirectionRight) {
296 io.AddMouseWheelEvent(1.0f, 0.0f); // Swipe Right
297 } else if (gesture.direction == UISwipeGestureRecognizerDirectionLeft) {
298 io.AddMouseWheelEvent(-1.0f, 0.0f); // Swipe Left
299 }
300 UIGestureRecognizer *gestureRecognizer = (UIGestureRecognizer *)gesture;
301 io.AddMousePosEvent([gestureRecognizer locationInView:self.view].x,
302 [gestureRecognizer locationInView:self.view].y);
303}
304
305- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture {
306 ImGuiIO &io = ImGui::GetIO();
307 io.AddMouseSourceEvent(ImGuiMouseSource_TouchScreen);
308 io.AddMouseButtonEvent(1, gesture.state == UIGestureRecognizerStateBegan);
309 UIGestureRecognizer *gestureRecognizer = (UIGestureRecognizer *)gesture;
310 io.AddMousePosEvent([gestureRecognizer locationInView:self.view].x,
311 [gestureRecognizer locationInView:self.view].y);
312}
313
314- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
315 shouldRecognizeSimultaneouslyWithGestureRecognizer:
316 (UIGestureRecognizer *)otherGestureRecognizer {
317 (void)gestureRecognizer;
318 (void)otherGestureRecognizer;
319 return YES;
320}
321
322#endif
323
324@end
325
326// ----------------------------------------------------------------------------
327// SceneDelegate (UIScene lifecycle)
328// ----------------------------------------------------------------------------
329
330#if !TARGET_OS_OSX
331
332@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
333@property(nonatomic, strong) UIWindow *window;
334@end
335
336@implementation SceneDelegate
337
338- (void)scene:(UIScene *)scene
339 willConnectToSession:(UISceneSession *)session
340 options:(UISceneConnectionOptions *)connectionOptions {
341 if (![scene isKindOfClass:[UIWindowScene class]]) {
342 return;
343 }
344 UIWindowScene *windowScene = (UIWindowScene *)scene;
345 UIViewController *rootViewController = [[AppViewController alloc] init];
346 self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
347 self.window.rootViewController = rootViewController;
348 [self.window makeKeyAndVisible];
349}
350
351@end
352
353#endif
354
355// ----------------------------------------------------------------------------
356// AppDelegate
357// ----------------------------------------------------------------------------
358
359#if TARGET_OS_OSX
360
361@interface AppDelegate : NSObject <NSApplicationDelegate>
362@property(nonatomic, strong) NSWindow *window;
363@end
364
365@implementation AppDelegate
366
367- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
368 return YES;
369}
370
371- (instancetype)init {
372 if (self = [super init]) {
373 NSViewController *rootViewController = [[AppViewController alloc] initWithNibName:nil
374 bundle:nil];
375 self.window = [[NSWindow alloc]
376 initWithContentRect:NSZeroRect
377 styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
378 NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable
379 backing:NSBackingStoreBuffered
380 defer:NO];
381 self.window.contentViewController = rootViewController;
382 [self.window center];
383 [self.window makeKeyAndOrderFront:self];
384 }
385 return self;
386}
387
388@end
389
390#else
391
392@interface AppDelegate : UIResponder <UIApplicationDelegate, UIDocumentPickerDelegate>
393@property(nonatomic, strong) UIWindow *window;
394@property(nonatomic, copy) void (^completionHandler)(NSString *selectedFile);
395@property(nonatomic, strong) UIDocumentPickerViewController *documentPicker;
396@end
397
398@implementation AppDelegate
399
400- (BOOL)application:(UIApplication *)application
401 didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions {
402 if (@available(iOS 13.0, *)) {
403 return YES;
404 }
405 UIViewController *rootViewController = [[AppViewController alloc] init];
406 self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
407 self.window.rootViewController = rootViewController;
408 [self.window makeKeyAndVisible];
409 return YES;
410}
411
412- (UISceneConfiguration *)application:(UIApplication *)application
413 configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession
414 options:(UISceneConnectionOptions *)options {
415 if (@available(iOS 13.0, *)) {
416 UISceneConfiguration *configuration =
417 [[UISceneConfiguration alloc] initWithName:@"Default Configuration"
418 sessionRole:connectingSceneSession.role];
419 configuration.delegateClass = [SceneDelegate class];
420 configuration.sceneClass = [UIWindowScene class];
421 return configuration;
422 }
423 return nil;
424}
425
426- (void)applicationWillTerminate:(UIApplication *)application {
428}
429
430- (UIViewController *)RootViewControllerForPresenting {
431 if (@available(iOS 13.0, *)) {
432 for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
433 if (scene.activationState != UISceneActivationStateForegroundActive) {
434 continue;
435 }
436 if (![scene isKindOfClass:[UIWindowScene class]]) {
437 continue;
438 }
439 UIWindowScene *windowScene = (UIWindowScene *)scene;
440 for (UIWindow *window in windowScene.windows) {
441 if (window.isKeyWindow && window.rootViewController) {
442 return window.rootViewController;
443 }
444 }
445 if (windowScene.windows.count > 0 &&
446 windowScene.windows.firstObject.rootViewController) {
447 return windowScene.windows.firstObject.rootViewController;
448 }
449 }
450 }
451
452 return self.window.rootViewController;
453}
454
455- (void)PresentDocumentPickerWithCompletionHandler:
456 (void (^)(NSString *selectedFile))completionHandler
457 allowedTypes:(NSArray<UTType*> *)allowedTypes {
458 self.completionHandler = completionHandler;
459
460 NSArray<UTType*>* documentTypes = allowedTypes;
461 if (!documentTypes || documentTypes.count == 0) {
462 documentTypes = @[ UTTypeData ];
463 }
464 UIViewController *rootViewController = [self RootViewControllerForPresenting];
465 if (!rootViewController) {
466 if (self.completionHandler) {
467 self.completionHandler(@"");
468 }
469 self.completionHandler = nil;
470 return;
471 }
472 _documentPicker =
473 [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:documentTypes];
474 _documentPicker.delegate = self;
475 _documentPicker.modalPresentationStyle = UIModalPresentationFormSheet;
476
477 [rootViewController presentViewController:_documentPicker animated:YES completion:nil];
478}
479
480- (void)documentPicker:(UIDocumentPickerViewController *)controller
481 didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls {
482 NSURL *selectedFileURL = [urls firstObject];
483
484 if (self.completionHandler) {
485 if (selectedFileURL) {
486 // Create a temporary file path
487 NSString *tempDir = NSTemporaryDirectory();
488 NSString *fileName = [selectedFileURL lastPathComponent];
489 NSString *tempPath = [tempDir stringByAppendingPathComponent:fileName];
490 NSURL *tempURL = [NSURL fileURLWithPath:tempPath];
491
492 // Copy the file to the temporary location
493 NSError *error = nil;
494 [[NSFileManager defaultManager] removeItemAtURL:tempURL error:nil]; // Remove if exists
495
496 [selectedFileURL startAccessingSecurityScopedResource];
497 BOOL success = [[NSFileManager defaultManager] copyItemAtURL:selectedFileURL toURL:tempURL error:&error];
498 [selectedFileURL stopAccessingSecurityScopedResource];
499
500 if (success) {
501 std::string cppPath = std::string([tempPath UTF8String]);
502 NSLog(@"File copied to temporary path: %s", cppPath.c_str());
503 self.completionHandler(tempPath);
504 } else {
505 NSLog(@"Failed to copy ROM to temp directory: %@", error);
506 self.completionHandler(@"");
507 }
508 } else {
509 self.completionHandler(@"");
510 }
511 }
512 self.completionHandler = nil;
513 [controller dismissViewControllerAnimated:YES completion:nil];
514}
515
516- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {
517 if (self.completionHandler) {
518 self.completionHandler(@"");
519 }
520 self.completionHandler = nil;
521 [controller dismissViewControllerAnimated:YES completion:nil];
522}
523
524@end
525
526#endif
527
528// ----------------------------------------------------------------------------
529// Application main() function
530// ----------------------------------------------------------------------------
531
532#if TARGET_OS_OSX
533int main(int argc, const char *argv[]) { return NSApplicationMain(argc, argv); }
534#else
535
536int main(int argc, char *argv[]) {
537 @autoreleasepool {
538 return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
539 }
540}
541
542#endif
Controller * GetController()
Definition application.h:73
static Application & Instance()
absl::Status Initialize(const IOSHostConfig &config)
Definition ios_host.mm:20
void SetMetalView(void *view)
Definition ios_host.mm:52
int main(int argc, char **argv)
Definition emu.cc:39
UIWindow * window
Definition main.mm:393
UIWindow * window
Definition main.mm:333
bool host_initialized_
Definition main.mm:63
UITouch * primary_touch_
Definition main.mm:64
yaze::ios::IOSHost g_ios_host
Definition main.mm:54
SDL2/SDL3 compatibility layer.
Configuration options for the application startup.
Definition application.h:24
std::string rom_file
Definition application.h:26