524 lines
17 KiB
Plaintext
524 lines
17 KiB
Plaintext
#include "platform/platform.h"
|
|
|
|
#import <Cocoa/Cocoa.h>
|
|
|
|
// 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 <NSApplicationDelegate>
|
|
@end
|
|
|
|
@implementation ASmplAppDelegate
|
|
- (void)applicationDidFinishLaunching:(NSNotification *)notification { (void)notification; }
|
|
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { (void)sender; return YES; }
|
|
@end
|
|
|
|
@interface ASmplWindowDelegate : NSObject <NSWindowDelegate> {
|
|
@public
|
|
PlatformWindow *_platformWindow;
|
|
}
|
|
@end
|
|
|
|
@interface ASmplView : NSView <NSTextInputClient> {
|
|
@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<NSAttributedStringKey> *)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;
|
|
}
|
|
}
|