begin macos port

This commit is contained in:
2026-03-03 10:00:38 -05:00
parent 7e298faadd
commit ad30ca8cb7
13 changed files with 2205 additions and 124 deletions

View File

@@ -0,0 +1,426 @@
#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,
};
static uint8_t macos_keycode_to_pkey(uint16_t 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_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;
default: return 0;
}
}
////////////////////////////////
// Forward declarations
struct PlatformWindow;
static PlatformWindow *g_current_window = nullptr;
////////////////////////////////
// Objective-C helper classes
@interface ASmplWindowDelegate : NSObject <NSWindowDelegate>
@end
@implementation ASmplWindowDelegate
- (BOOL)windowShouldClose:(id)sender {
(void)sender;
if (g_current_window) {
// Set should_close flag (accessed via the struct below)
extern void platform_macos_set_should_close();
platform_macos_set_should_close();
}
return NO; // We handle closing ourselves
}
- (void)windowDidResize:(NSNotification *)notification {
(void)notification;
if (!g_current_window) return;
extern void platform_macos_handle_resize();
platform_macos_handle_resize();
}
@end
@interface ASmplView : NSView <NSTextInputClient>
@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;
extern void platform_macos_insert_text(const char *utf8);
platform_macos_insert_text([str UTF8String]);
}
- (void)keyDown:(NSEvent *)event {
extern void platform_macos_key_down(uint16_t keycode, NSEventModifierFlags mods);
platform_macos_key_down([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;
extern void platform_macos_mouse_down();
platform_macos_mouse_down();
}
- (void)mouseUp:(NSEvent *)event {
(void)event;
extern void platform_macos_mouse_up();
platform_macos_mouse_up();
}
- (void)mouseMoved:(NSEvent *)event { (void)event; }
- (void)mouseDragged:(NSEvent *)event { (void)event; }
- (void)scrollWheel:(NSEvent *)event {
extern void platform_macos_scroll(float dx, float dy);
float dy = (float)[event scrollingDeltaY];
if ([event hasPreciseScrollingDeltas])
dy /= 40.0f; // Normalize trackpad deltas to match discrete wheel steps
platform_macos_scroll(0, dy);
}
- (BOOL)acceptsFirstMouse:(NSEvent *)event { (void)event; return YES; }
@end
////////////////////////////////
// PlatformWindow struct
struct PlatformWindow {
NSWindow *ns_window;
ASmplView *view;
ASmplWindowDelegate *delegate;
bool should_close;
int32_t width;
int32_t height;
int32_t 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)
void platform_macos_set_should_close() {
if (g_current_window) g_current_window->should_close = true;
}
void platform_macos_handle_resize() {
if (!g_current_window) return;
NSRect frame = [g_current_window->view bounds];
F32 scale = g_current_window->backing_scale;
g_current_window->width = (int32_t)(frame.size.width * scale);
g_current_window->height = (int32_t)(frame.size.height * scale);
if (g_current_window->frame_callback)
g_current_window->frame_callback(g_current_window->frame_callback_user_data);
}
void platform_macos_insert_text(const char *utf8) {
if (!g_current_window || !utf8) return;
PlatformInput *ev = &g_current_window->input;
while (*utf8 && ev->char_count < PLATFORM_MAX_CHARS_PER_FRAME) {
uint8_t c = (uint8_t)*utf8;
if (c < 32) { utf8++; continue; }
// Handle ASCII printable range (single-byte UTF-8)
if (c < 0x80) {
ev->chars[ev->char_count++] = (uint16_t)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;
}
}
}
void platform_macos_key_down(uint16_t keycode, NSEventModifierFlags mods) {
if (!g_current_window) return;
PlatformInput *ev = &g_current_window->input;
uint8_t 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;
}
void platform_macos_mouse_down() {
if (g_current_window) g_current_window->mouse_down_state = 1;
}
void platform_macos_mouse_up() {
if (g_current_window) g_current_window->mouse_down_state = 0;
}
void platform_macos_scroll(float dx, float dy) {
(void)dx;
if (g_current_window) g_current_window->input.scroll_delta.y += dy;
}
////////////////////////////////
// Menu action handler
@interface ASmplMenuTarget : NSObject
- (void)menuAction:(id)sender;
@end
@implementation ASmplMenuTarget
- (void)menuAction:(id)sender {
if (!g_current_window) return;
NSMenuItem *item = (NSMenuItem *)sender;
g_current_window->pending_menu_cmd = (int32_t)[item tag];
}
@end
static ASmplMenuTarget *g_menu_target = nil;
////////////////////////////////
// Public API
PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
// Ensure NSApplication is initialized
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
NSRect content_rect = NSMakeRect(0, 0, desc->width, desc->height);
NSWindow *ns_window = [[NSWindow alloc]
initWithContentRect:content_rect
styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable)
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 = (int32_t)(desc->width * window->backing_scale);
window->height = (int32_t)(desc->height * window->backing_scale);
g_current_window = window;
[ns_window makeKeyAndOrderFront:nil];
[NSApp activateIgnoringOtherApps:YES];
return window;
}
void platform_destroy_window(PlatformWindow *window) {
if (!window) return;
[window->ns_window close];
if (g_current_window == window)
g_current_window = nullptr;
delete window;
}
bool platform_poll_events(PlatformWindow *window) {
@autoreleasepool {
NSEvent *event;
while ((event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:nil
inMode:NSDefaultRunLoopMode
dequeue:YES])) {
[NSApp sendEvent:event];
}
}
return !window->should_close;
}
void platform_get_size(PlatformWindow *window, int32_t *w, int32_t *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, int32_t 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 (int32_t i = 0; i < menu_count; i++) {
NSMenuItem *top_item = [[NSMenuItem alloc] init];
NSMenu *submenu = [[NSMenu alloc] initWithTitle:
[NSString stringWithUTF8String:menus[i].label]];
for (int32_t 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];
}
int32_t platform_poll_menu_command(PlatformWindow *window) {
int32_t 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;
// Clear accumulated events for next frame
window->input = {};
return result;
}
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;
}