#include "platform/platform.h" #import // macOS virtual key codes (avoids Carbon.h include) enum { kVK_ANSI_A = 0x00, kVK_ANSI_C = 0x08, kVK_ANSI_V = 0x09, kVK_ANSI_X = 0x07, kVK_Return = 0x24, kVK_Tab = 0x30, kVK_Delete = 0x33, kVK_Escape = 0x35, kVK_ForwardDelete = 0x75, kVK_LeftArrow = 0x7B, kVK_RightArrow = 0x7C, kVK_DownArrow = 0x7D, kVK_UpArrow = 0x7E, kVK_Home = 0x73, kVK_End = 0x77, kVK_Command = 0x37, kVK_Shift = 0x38, kVK_RightShift = 0x3C, kVK_RightCommand = 0x36, kVK_ANSI_Equal = 0x18, kVK_ANSI_Minus = 0x1B, kVK_ANSI_0 = 0x1D, kVK_ANSI_KeypadEnter = 0x4C, }; static U8 macos_keycode_to_pkey(U16 keycode) { switch (keycode) { case kVK_ANSI_A: return PKEY_A; case kVK_ANSI_C: return PKEY_C; case kVK_ANSI_V: return PKEY_V; case kVK_ANSI_X: return PKEY_X; case kVK_Return: return PKEY_RETURN; case kVK_ANSI_KeypadEnter: return PKEY_RETURN; case kVK_Tab: return PKEY_TAB; case kVK_Delete: return PKEY_BACKSPACE; case kVK_ForwardDelete:return PKEY_DELETE; case kVK_Escape: return PKEY_ESCAPE; case kVK_LeftArrow: return PKEY_LEFT; case kVK_RightArrow: return PKEY_RIGHT; case kVK_UpArrow: return PKEY_UP; case kVK_DownArrow: return PKEY_DOWN; case kVK_Home: return PKEY_HOME; case kVK_End: return PKEY_END; case kVK_ANSI_Equal: return PKEY_EQUAL; case kVK_ANSI_Minus: return PKEY_MINUS; case kVK_ANSI_0: return PKEY_0; default: return 0; } } //////////////////////////////// // Forward declarations struct PlatformWindow; // Main window receives menu commands static PlatformWindow *g_main_window = nullptr; //////////////////////////////// // Objective-C helper classes @interface ASmplAppDelegate : NSObject @end @implementation ASmplAppDelegate - (void)applicationDidFinishLaunching:(NSNotification *)notification { (void)notification; } - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { (void)sender; return YES; } @end @interface ASmplWindowDelegate : NSObject { @public PlatformWindow *_platformWindow; } @end @interface ASmplView : NSView { @public PlatformWindow *_platformWindow; } @end //////////////////////////////// // PlatformWindow struct struct PlatformWindow { NSWindow *ns_window; ASmplView *view; ASmplWindowDelegate *delegate; B32 should_close; S32 width; S32 height; S32 pending_menu_cmd; PlatformFrameCallback frame_callback; void *frame_callback_user_data; PlatformInput input; B32 prev_mouse_down; B32 mouse_down_state; F32 backing_scale; }; //////////////////////////////// // C callback helpers (called from ObjC via PlatformWindow pointer) static void platform_macos_insert_text_pw(PlatformWindow *pw, const char *utf8) { if (!pw || !utf8) return; PlatformInput *ev = &pw->input; while (*utf8 && ev->char_count < PLATFORM_MAX_CHARS_PER_FRAME) { U8 c = (U8)*utf8; if (c < 32) { utf8++; continue; } // Handle ASCII printable range (single-byte UTF-8) if (c < 0x80) { ev->chars[ev->char_count++] = (U16)c; utf8++; } else { // Skip multi-byte UTF-8 sequences for now (UI only handles ASCII) if (c < 0xE0) utf8 += 2; else if (c < 0xF0) utf8 += 3; else utf8 += 4; } } } static void platform_macos_key_down_pw(PlatformWindow *pw, U16 keycode, NSEventModifierFlags mods) { if (!pw) return; PlatformInput *ev = &pw->input; U8 pkey = macos_keycode_to_pkey(keycode); if (pkey && ev->key_count < PLATFORM_MAX_KEYS_PER_FRAME) ev->keys[ev->key_count++] = pkey; // Command = ctrl_held (macOS convention: Cmd+C, Cmd+V, etc.) ev->ctrl_held = (mods & NSEventModifierFlagCommand) != 0; ev->shift_held = (mods & NSEventModifierFlagShift) != 0; } //////////////////////////////// // ObjC class implementations @implementation ASmplWindowDelegate - (BOOL)windowShouldClose:(id)sender { (void)sender; if (_platformWindow) { _platformWindow->should_close = true; } return NO; // We handle closing ourselves } - (void)windowDidResize:(NSNotification *)notification { (void)notification; if (!_platformWindow) return; PlatformWindow *pw = _platformWindow; NSRect frame = [pw->view bounds]; F32 scale = pw->backing_scale; pw->width = (S32)(frame.size.width * scale); pw->height = (S32)(frame.size.height * scale); if (pw->frame_callback) pw->frame_callback(pw->frame_callback_user_data); } @end @implementation ASmplView - (BOOL)acceptsFirstResponder { return YES; } - (BOOL)canBecomeKeyView { return YES; } // Needed for NSTextInputClient - (BOOL)hasMarkedText { return NO; } - (NSRange)markedRange { return NSMakeRange(NSNotFound, 0); } - (NSRange)selectedRange { return NSMakeRange(NSNotFound, 0); } - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { (void)string; (void)selectedRange; (void)replacementRange; } - (void)unmarkText {} - (NSArray *)validAttributesForMarkedText { return @[]; } - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange { (void)range; (void)actualRange; return nil; } - (NSUInteger)characterIndexForPoint:(NSPoint)point { (void)point; return NSNotFound; } - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange { (void)range; (void)actualRange; return NSZeroRect; } - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { (void)replacementRange; NSString *str = nil; if ([string isKindOfClass:[NSAttributedString class]]) str = [string string]; else str = (NSString *)string; platform_macos_insert_text_pw(_platformWindow, [str UTF8String]); } - (void)keyDown:(NSEvent *)event { platform_macos_key_down_pw(_platformWindow, [event keyCode], [event modifierFlags]); // Feed into text input system for character generation [self interpretKeyEvents:@[event]]; } - (void)flagsChanged:(NSEvent *)event { (void)event; // Modifiers are read at key-down time, nothing to accumulate here } - (void)mouseDown:(NSEvent *)event { (void)event; if (_platformWindow) _platformWindow->mouse_down_state = 1; } - (void)mouseUp:(NSEvent *)event { (void)event; if (_platformWindow) _platformWindow->mouse_down_state = 0; } - (void)mouseMoved:(NSEvent *)event { (void)event; } - (void)mouseDragged:(NSEvent *)event { (void)event; } - (void)scrollWheel:(NSEvent *)event { if (!_platformWindow) return; F32 dy = (F32)[event scrollingDeltaY]; if ([event hasPreciseScrollingDeltas]) dy /= 40.0f; // Normalize trackpad deltas to match discrete wheel steps _platformWindow->input.scroll_delta.y += dy; } - (BOOL)acceptsFirstMouse:(NSEvent *)event { (void)event; return YES; } @end //////////////////////////////// // Menu action handler @interface ASmplMenuTarget : NSObject - (void)menuAction:(id)sender; @end @implementation ASmplMenuTarget - (void)menuAction:(id)sender { if (!g_main_window) return; NSMenuItem *item = (NSMenuItem *)sender; g_main_window->pending_menu_cmd = (S32)[item tag]; } @end static ASmplMenuTarget *g_menu_target = nil; //////////////////////////////// // Public API PlatformWindow *platform_create_window(PlatformWindowDesc *desc) { // Ensure NSApplication is initialized and properly launched [NSApplication sharedApplication]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; static ASmplAppDelegate *app_delegate = nil; if (!app_delegate) { app_delegate = [[ASmplAppDelegate alloc] init]; [NSApp setDelegate:app_delegate]; [NSApp finishLaunching]; } NSRect content_rect = NSMakeRect(0, 0, desc->width, desc->height); NSWindowStyleMask style_mask; if (desc->style == PLATFORM_WINDOW_STYLE_POPUP) { style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable; } else { style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; } NSWindow *ns_window = [[NSWindow alloc] initWithContentRect:content_rect styleMask:style_mask backing:NSBackingStoreBuffered defer:NO]; [ns_window setTitle:[NSString stringWithUTF8String:desc->title]]; [ns_window center]; ASmplView *view = [[ASmplView alloc] initWithFrame:content_rect]; [ns_window setContentView:view]; [ns_window makeFirstResponder:view]; // Enable mouse moved events [ns_window setAcceptsMouseMovedEvents:YES]; ASmplWindowDelegate *delegate = [[ASmplWindowDelegate alloc] init]; [ns_window setDelegate:delegate]; PlatformWindow *window = new PlatformWindow(); memset(window, 0, sizeof(*window)); window->ns_window = ns_window; window->view = view; window->delegate = delegate; window->should_close = false; window->backing_scale = (F32)[ns_window backingScaleFactor]; window->width = (S32)(desc->width * window->backing_scale); window->height = (S32)(desc->height * window->backing_scale); // Wire up per-window pointers view->_platformWindow = window; delegate->_platformWindow = window; // Track main window for menu commands if (desc->style == PLATFORM_WINDOW_STYLE_NORMAL) { g_main_window = window; } // If popup, add as child of parent if (desc->style == PLATFORM_WINDOW_STYLE_POPUP && desc->parent) { [desc->parent->ns_window addChildWindow:ns_window ordered:NSWindowAbove]; } [ns_window makeKeyAndOrderFront:nil]; [NSApp activateIgnoringOtherApps:YES]; return window; } void platform_destroy_window(PlatformWindow *window) { if (!window) return; // Remove from parent if it's a child window NSWindow *parent = [window->ns_window parentWindow]; if (parent) { [parent removeChildWindow:window->ns_window]; } [window->ns_window close]; if (g_main_window == window) g_main_window = nullptr; delete window; } B32 platform_poll_events(PlatformWindow *window) { @autoreleasepool { // When the app is not active (e.g. during Cmd+Tab away), block until // an event arrives instead of spinning. This keeps CPU near zero while // backgrounded and lets macOS handle the app-switch animation smoothly. BOOL is_active = [NSApp isActive]; NSDate *deadline = is_active ? nil : [NSDate distantFuture]; NSEvent *event; while ((event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:deadline inMode:NSDefaultRunLoopMode dequeue:YES])) { [NSApp sendEvent:event]; // After processing one event while backgrounded, switch to // non-blocking drain so we handle any remaining queued events // (including the activation event that will flip isActive). deadline = nil; } } return !window->should_close; } void platform_get_size(PlatformWindow *window, S32 *w, S32 *h) { if (w) *w = window->width; if (h) *h = window->height; } void *platform_get_native_handle(PlatformWindow *window) { return (__bridge void *)window->view; } void platform_set_frame_callback(PlatformWindow *window, PlatformFrameCallback cb, void *user_data) { window->frame_callback = cb; window->frame_callback_user_data = user_data; } void platform_set_menu(PlatformWindow *window, PlatformMenu *menus, S32 menu_count) { (void)window; if (!g_menu_target) g_menu_target = [[ASmplMenuTarget alloc] init]; NSMenu *menu_bar = [[NSMenu alloc] init]; // App menu (required on macOS) NSMenuItem *app_menu_item = [[NSMenuItem alloc] init]; NSMenu *app_menu = [[NSMenu alloc] init]; [app_menu addItemWithTitle:@"Quit autosample" action:@selector(terminate:) keyEquivalent:@"q"]; [app_menu_item setSubmenu:app_menu]; [menu_bar addItem:app_menu_item]; for (S32 i = 0; i < menu_count; i++) { NSMenuItem *top_item = [[NSMenuItem alloc] init]; NSMenu *submenu = [[NSMenu alloc] initWithTitle: [NSString stringWithUTF8String:menus[i].label]]; for (S32 j = 0; j < menus[i].item_count; j++) { PlatformMenuItem *item = &menus[i].items[j]; if (!item->label) { [submenu addItem:[NSMenuItem separatorItem]]; } else { NSMenuItem *ns_item = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:item->label] action:@selector(menuAction:) keyEquivalent:@""]; [ns_item setTag:item->id]; [ns_item setTarget:g_menu_target]; [submenu addItem:ns_item]; } } [top_item setSubmenu:submenu]; [menu_bar addItem:top_item]; } [NSApp setMainMenu:menu_bar]; } S32 platform_poll_menu_command(PlatformWindow *window) { S32 cmd = window->pending_menu_cmd; window->pending_menu_cmd = 0; return cmd; } PlatformInput platform_get_input(PlatformWindow *window) { PlatformInput result = window->input; // Poll mouse position (Cocoa uses bottom-left origin, flip Y) NSPoint mouse_in_window = [window->ns_window mouseLocationOutsideOfEventStream]; NSRect view_bounds = [window->view bounds]; F32 scale = window->backing_scale; result.mouse_pos = v2f32( (F32)mouse_in_window.x * scale, (F32)(view_bounds.size.height - mouse_in_window.y) * scale); // Mouse button state result.was_mouse_down = window->prev_mouse_down; result.mouse_down = window->mouse_down_state; window->prev_mouse_down = result.mouse_down; // Poll current modifier state (so shift/ctrl are accurate even without key events) NSEventModifierFlags mods = [NSEvent modifierFlags]; result.ctrl_held = (mods & NSEventModifierFlagCommand) != 0; result.shift_held = (mods & NSEventModifierFlagShift) != 0; // Clear accumulated events for next frame window->input = {}; return result; } B32 platform_window_should_close(PlatformWindow *window) { return window ? window->should_close : 0; } F32 platform_get_dpi_scale(PlatformWindow *window) { (void)window; return 1.0f; // macOS handles Retina via backing scale factor, not DPI } void platform_set_cursor(PlatformCursor cursor) { switch (cursor) { case PLATFORM_CURSOR_SIZE_WE: [[NSCursor resizeLeftRightCursor] set]; break; case PLATFORM_CURSOR_SIZE_NS: [[NSCursor resizeUpDownCursor] set]; break; default: [[NSCursor arrowCursor] set]; break; } } void platform_clipboard_set(const char *text) { if (!text) return; NSPasteboard *pb = [NSPasteboard generalPasteboard]; [pb clearContents]; [pb setString:[NSString stringWithUTF8String:text] forType:NSPasteboardTypeString]; } const char *platform_clipboard_get() { static char buf[4096]; buf[0] = '\0'; NSPasteboard *pb = [NSPasteboard generalPasteboard]; NSString *str = [pb stringForType:NSPasteboardTypeString]; if (str) { const char *utf8 = [str UTF8String]; if (utf8) { size_t len = strlen(utf8); if (len >= sizeof(buf)) len = sizeof(buf) - 1; memcpy(buf, utf8, len); buf[len] = '\0'; } } return buf[0] ? buf : nullptr; } S32 platform_message_box(PlatformWindow *parent, const char *title, const char *message, PlatformMsgBoxType type) { @autoreleasepool { NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:[NSString stringWithUTF8String:title]]; [alert setInformativeText:[NSString stringWithUTF8String:message]]; switch (type) { case PLATFORM_MSGBOX_OK: [alert addButtonWithTitle:@"OK"]; break; case PLATFORM_MSGBOX_OK_CANCEL: [alert addButtonWithTitle:@"OK"]; [alert addButtonWithTitle:@"Cancel"]; break; case PLATFORM_MSGBOX_YES_NO: [alert addButtonWithTitle:@"Yes"]; [alert addButtonWithTitle:@"No"]; break; } NSModalResponse response; if (parent && parent->ns_window) { response = [alert runModal]; } else { response = [alert runModal]; } // NSAlertFirstButtonReturn = 1000, Second = 1001 if (response == NSAlertFirstButtonReturn) return 0; if (response == NSAlertSecondButtonReturn) return 1; return -1; } }