From 5eaae4deb95be9f0236b3754ac2305739f22ab7e Mon Sep 17 00:00:00 2001 From: Max Amundsen Date: Thu, 5 Mar 2026 11:44:23 -0500 Subject: [PATCH] Use platform windows for windowing instead of custom draggable window system --- .vscode/launch.json | 4 +- src/main.cpp | 199 ++++-------------- src/platform/platform.h | 26 ++- src/platform/platform_macos.mm | 267 ++++++++++++++---------- src/platform/platform_win32.cpp | 137 ++++++++---- src/renderer/renderer.h | 7 + src/renderer/renderer_dx12.cpp | 111 ++++++++-- src/renderer/renderer_metal.mm | 80 ++++++- src/ui/ui_core.h | 7 - src/ui/ui_piano.cpp | 136 ++++++++++++ src/ui/ui_piano.h | 20 ++ src/ui/ui_popups.cpp | 143 +++++++++++++ src/ui/ui_popups.h | 31 +++ src/ui/ui_widgets.cpp | 357 -------------------------------- src/ui/ui_widgets.h | 50 +---- 15 files changed, 833 insertions(+), 742 deletions(-) create mode 100644 src/ui/ui_piano.cpp create mode 100644 src/ui/ui_piano.h create mode 100644 src/ui/ui_popups.cpp create mode 100644 src/ui/ui_popups.h diff --git a/.vscode/launch.json b/.vscode/launch.json index f6a7815..8488fe3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "program": "${workspaceFolder}/build_debug/autosample.app/Contents/MacOS/autosample", "args": [], "cwd": "${workspaceFolder}", - "preLaunchTask": "build-debug" + // "preLaunchTask": "build-debug" }, { "name": "Debug autosample (Windows)", @@ -18,7 +18,7 @@ "args": [], "cwd": "${workspaceFolder}", "console": "integratedTerminal", - "preLaunchTask": "build-debug" + // "preLaunchTask": "build-debug" } ] } diff --git a/src/main.cpp b/src/main.cpp index c78823d..1afce2c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -12,12 +12,16 @@ #include "ui/ui_core.h" #include "ui/ui_icons.h" #include "ui/ui_widgets.h" +#include "ui/ui_popups.h" +#include "ui/ui_piano.h" // [cpp] #include "base/base_inc.cpp" #include "ui/ui_core.cpp" #include "ui/ui_icons.cpp" #include "ui/ui_widgets.cpp" +#include "ui/ui_popups.cpp" +#include "ui/ui_piano.cpp" #ifdef __APPLE__ #include "platform/platform_macos.mm" #include "renderer/renderer_metal.mm" @@ -83,7 +87,7 @@ struct AppState { S32 bottom_panel_tab; // 0 = Item Editor, 1 = Sample Mapper // Piano state - S32 piano_mouse_note; // MIDI note held by mouse click (-1 = none) + UI_PianoState piano_state; // Demo widget state B32 demo_checkbox_a; @@ -159,68 +163,6 @@ struct AppState { }; //////////////////////////////// -// Piano helpers - -#define PIANO_FIRST_NOTE 21 // A0 -#define PIANO_LAST_NOTE 108 // C8 -#define PIANO_BLACK_W 11.0f -#define PIANO_BLACK_H_PCT 0.6f - -static B32 piano_is_black_key(S32 note) { - S32 n = note % 12; - return n == 1 || n == 3 || n == 6 || n == 8 || n == 10; -} - -// Velocity-based color: blue (vel 0) → green (mid) → red (vel 127) -static Clay_Color velocity_color(S32 velocity) { - F32 t = (F32)velocity / 127.0f; - F32 r, g, b; - if (t < 0.5f) { - F32 s = t * 2.0f; - r = 40.0f + s * (76.0f - 40.0f); - g = 120.0f + s * (175.0f - 120.0f); - b = 220.0f + s * (80.0f - 220.0f); - } else { - F32 s = (t - 0.5f) * 2.0f; - r = 76.0f + s * (220.0f - 76.0f); - g = 175.0f + s * (50.0f - 175.0f); - b = 80.0f + s * (40.0f - 80.0f); - } - return Clay_Color{r, g, b, 255}; -} - -// Piano input: handle mouse clicks on piano keys -static void update_piano_input(AppState *app) { - if (app->master_layout != 0) return; - PlatformInput input = g_wstate.input; - - if (!input.mouse_down) { - app->piano_mouse_note = -1; - return; - } - - // Find hovered piano key — check black keys first (they're on top) - S32 hovered_note = -1; - for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) { - if (piano_is_black_key(note) && Clay_PointerOver(CLAY_IDI("PKey", note))) { - hovered_note = note; - break; - } - } - if (hovered_note == -1) { - for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) { - if (!piano_is_black_key(note) && Clay_PointerOver(CLAY_IDI("PKey", note))) { - hovered_note = note; - break; - } - } - } - - if (hovered_note != -1) { - app->piano_mouse_note = hovered_note; - } -} - //////////////////////////////// // Panel builders @@ -654,7 +596,7 @@ static void build_right_panel(AppState *app) { // Idle = dark gray Clay_Color box_color; if (dev->active) { - box_color = velocity_color(dev->velocity); + box_color = piano_velocity_color(dev->velocity); } else if (dev->releasing) { box_color = Clay_Color{255, 255, 255, 255}; } else { @@ -791,83 +733,9 @@ static void build_log_panel(AppState *app) { .layoutDirection = CLAY_TOP_TO_BOTTOM, } ) { - Clay_ElementId piano_id = CLAY_ID("PianoContainer"); - CLAY(piano_id, - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, - .layoutDirection = CLAY_LEFT_TO_RIGHT, - } - ) { - // Compute black key size proportional to white keys - F32 piano_avail_h = uis(app->log_height) - TAB_HEIGHT - uip(8); - F32 black_key_h = piano_avail_h * PIANO_BLACK_H_PCT; - if (black_key_h < uis(20)) black_key_h = uis(20); - - F32 white_key_w = ((F32)app->last_w - uip(8)) / 52.0f; - F32 black_key_w = white_key_w * 0.6f; - if (black_key_w < uis(8)) black_key_w = uis(8); - - // White keys (grow to fill width and height) - for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) { - if (piano_is_black_key(note)) continue; - - B32 midi_held = midi_is_note_held(app->midi, note); - B32 mouse_held = app->piano_mouse_note == note; - Clay_Color bg; - if (midi_held) { - bg = velocity_color(midi_get_note_velocity(app->midi, note)); - } else if (mouse_held) { - bg = g_theme.accent; - } else { - bg = Clay_Color{240, 240, 240, 255}; - } - - CLAY(CLAY_IDI("PKey", note), - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, - }, - .backgroundColor = bg, - .border = { .color = {190, 190, 190, 255}, .width = { .right = 1 } }, - ) {} - } - - // Black keys (floating, attached to left white key) - for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) { - if (!piano_is_black_key(note)) continue; - - Clay_ElementId parent_wkey = CLAY_IDI("PKey", note - 1); - B32 midi_held = midi_is_note_held(app->midi, note); - B32 mouse_held = app->piano_mouse_note == note; - Clay_Color bg; - if (midi_held) { - bg = velocity_color(midi_get_note_velocity(app->midi, note)); - } else if (mouse_held) { - bg = g_theme.accent; - } else { - bg = Clay_Color{25, 25, 30, 255}; - } - - CLAY(CLAY_IDI("PKey", note), - .layout = { - .sizing = { - .width = CLAY_SIZING_FIXED(black_key_w), - .height = CLAY_SIZING_FIXED(black_key_h), - }, - }, - .backgroundColor = bg, - .cornerRadius = { .topLeft = 0, .topRight = 0, .bottomLeft = uis(2), .bottomRight = uis(2) }, - .floating = { - .parentId = parent_wkey.id, - .zIndex = 100, - .attachPoints = { - .element = CLAY_ATTACH_POINT_CENTER_TOP, - .parent = CLAY_ATTACH_POINT_RIGHT_TOP, - }, - .attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID, - }, - ) {} - } - } + F32 piano_avail_h = uis(app->log_height) - TAB_HEIGHT - uip(8); + F32 piano_avail_w = (F32)app->last_w - uip(8); + ui_piano(&app->piano_state, app->midi, piano_avail_w, piano_avail_h); } } } @@ -1882,25 +1750,6 @@ static void build_ui(AppState *app) { } } - // Draggable windows (rendered as floating elements above normal UI) - ui_window("WinSettings", "Preferences", &app->show_settings_window, - Vec2F32{100, 100}, Vec2F32{480, 0}, - settings_window_content, app); - - ui_window("WinAbout", "About", &app->show_about_window, - Vec2F32{200, 150}, Vec2F32{260, 0}, - about_window_content, nullptr); - - // Modal confirmation dialog - if (app->show_confirm_dialog) { - static const char *confirm_buttons[] = { "Cancel", "OK" }; - S32 modal_result = ui_modal("ModalConfirm", "Confirm Action", - "Are you sure you want to proceed? This action cannot be undone.", - confirm_buttons, 2); - if (modal_result != -1) { - app->show_confirm_dialog = 0; - } - } } //////////////////////////////// @@ -1990,7 +1839,7 @@ static void do_frame(AppState *app) { } ui_widgets_begin_frame(input); - update_piano_input(app); + if (app->master_layout == 0) ui_piano_update_input(&app->piano_state); update_panel_splitters(app); // Build UI with Clay @@ -2071,7 +1920,7 @@ int main(int argc, char **argv) { app.show_log = 1; app.show_midi_devices = 1; app.bottom_panel_tab = 0; - app.piano_mouse_note = -1; + app.piano_state.mouse_note = -1; app.demo_knob_unsigned = 75.0f; app.demo_knob_signed = 0.0f; app.demo_slider_h = 50.0f; @@ -2116,9 +1965,35 @@ int main(int argc, char **argv) { default: break; } + // Native confirmation dialog (blocking) + if (app.show_confirm_dialog) { + S32 result = platform_message_box(window, "Confirm Action", + "Are you sure you want to proceed? This action cannot be undone.", + PLATFORM_MSGBOX_OK_CANCEL); + app.show_confirm_dialog = 0; + (void)result; // result: 0 = OK, 1 = Cancel + } + + // Open popup windows when flags transition to 1 + if (app.show_settings_window && !popup_find_by_flag(&app.show_settings_window)) + popup_open(window, renderer, "Preferences", &app.show_settings_window, 480, 400, settings_window_content, &app); + if (app.show_about_window && !popup_find_by_flag(&app.show_about_window)) + popup_open(window, renderer, "About", &app.show_about_window, 260, 200, about_window_content, nullptr); + + // Check for OS close on popups + popup_close_check(); + if (running) do_frame(&app); + + // Render popup windows + // Compute dt for popups (same as main frame) + F32 popup_dt = 1.0f / 60.0f; // approximate; popups don't need precise timing + for (S32 i = 0; i < MAX_POPUP_WINDOWS; i++) { + if (g_popups[i].alive) popup_do_frame(&g_popups[i], popup_dt); + } } + popup_close_all(); audio_destroy(audio); midi_destroy(midi); ui_destroy(ui); diff --git a/src/platform/platform.h b/src/platform/platform.h index 28c367d..6c15d9b 100644 --- a/src/platform/platform.h +++ b/src/platform/platform.h @@ -53,10 +53,23 @@ struct PlatformInput { struct PlatformWindow; +enum PlatformWindowStyle { + PLATFORM_WINDOW_STYLE_NORMAL = 0, + PLATFORM_WINDOW_STYLE_POPUP = 1, // utility panel, owned by parent +}; + struct PlatformWindowDesc { - const char *title = "autosample"; - S32 width = 1280; - S32 height = 720; + const char *title = "autosample"; + S32 width = 1280; + S32 height = 720; + PlatformWindowStyle style = PLATFORM_WINDOW_STYLE_NORMAL; + PlatformWindow *parent = nullptr; +}; + +enum PlatformMsgBoxType { + PLATFORM_MSGBOX_OK = 0, + PLATFORM_MSGBOX_OK_CANCEL = 1, + PLATFORM_MSGBOX_YES_NO = 2, }; struct PlatformMenuItem { @@ -88,6 +101,13 @@ S32 platform_poll_menu_command(PlatformWindow *window); // Returns accumulated input since last call (keyboard events + polled mouse state), then clears the buffer. PlatformInput platform_get_input(PlatformWindow *window); +// Returns true if the window's close button was clicked (for popup windows). +B32 platform_window_should_close(PlatformWindow *window); + +// Blocks until user responds. Returns 0=first button, 1=second button, -1=dismissed. +S32 platform_message_box(PlatformWindow *parent, const char *title, + const char *message, PlatformMsgBoxType type); + // Cursor shapes for resize handles enum PlatformCursor { PLATFORM_CURSOR_ARROW = 0, diff --git a/src/platform/platform_macos.mm b/src/platform/platform_macos.mm index 312360f..40017c3 100644 --- a/src/platform/platform_macos.mm +++ b/src/platform/platform_macos.mm @@ -47,7 +47,9 @@ static U8 macos_keycode_to_pkey(U16 keycode) { // Forward declarations struct PlatformWindow; -static PlatformWindow *g_current_window = nullptr; + +// Main window receives menu commands +static PlatformWindow *g_main_window = nullptr; //////////////////////////////// // Objective-C helper classes @@ -60,31 +62,97 @@ static PlatformWindow *g_current_window = nullptr; - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { (void)sender; return YES; } @end -@interface ASmplWindowDelegate : NSObject +@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 (g_current_window) { - // Set should_close flag (accessed via the struct below) - extern void platform_macos_set_should_close(); - platform_macos_set_should_close(); + if (_platformWindow) { + _platformWindow->should_close = true; } 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(); + 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 -@interface ASmplView : NSView -@end - @implementation ASmplView - (BOOL)acceptsFirstResponder { return YES; } @@ -117,13 +185,11 @@ static PlatformWindow *g_current_window = nullptr; else str = (NSString *)string; - extern void platform_macos_insert_text(const char *utf8); - platform_macos_insert_text([str UTF8String]); + platform_macos_insert_text_pw(_platformWindow, [str UTF8String]); } - (void)keyDown:(NSEvent *)event { - extern void platform_macos_key_down(U16 keycode, NSEventModifierFlags mods); - platform_macos_key_down([event keyCode], [event modifierFlags]); + platform_macos_key_down_pw(_platformWindow, [event keyCode], [event modifierFlags]); // Feed into text input system for character generation [self interpretKeyEvents:@[event]]; @@ -136,112 +202,29 @@ static PlatformWindow *g_current_window = nullptr; - (void)mouseDown:(NSEvent *)event { (void)event; - extern void platform_macos_mouse_down(); - platform_macos_mouse_down(); + if (_platformWindow) _platformWindow->mouse_down_state = 1; } - (void)mouseUp:(NSEvent *)event { (void)event; - extern void platform_macos_mouse_up(); - platform_macos_mouse_up(); + if (_platformWindow) _platformWindow->mouse_down_state = 0; } - (void)mouseMoved:(NSEvent *)event { (void)event; } - (void)mouseDragged:(NSEvent *)event { (void)event; } - (void)scrollWheel:(NSEvent *)event { - extern void platform_macos_scroll(F32 dx, F32 dy); + if (!_platformWindow) return; F32 dy = (F32)[event scrollingDeltaY]; if ([event hasPreciseScrollingDeltas]) dy /= 40.0f; // Normalize trackpad deltas to match discrete wheel steps - platform_macos_scroll(0, dy); + _platformWindow->input.scroll_delta.y += dy; } - (BOOL)acceptsFirstMouse:(NSEvent *)event { (void)event; return YES; } @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) - -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 = (S32)(frame.size.width * scale); - g_current_window->height = (S32)(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) { - 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; - } - } -} - -void platform_macos_key_down(U16 keycode, NSEventModifierFlags mods) { - if (!g_current_window) return; - PlatformInput *ev = &g_current_window->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; -} - -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(F32 dx, F32 dy) { - (void)dx; - if (g_current_window) g_current_window->input.scroll_delta.y += dy; -} - //////////////////////////////// // Menu action handler @@ -251,9 +234,9 @@ void platform_macos_scroll(F32 dx, F32 dy) { @implementation ASmplMenuTarget - (void)menuAction:(id)sender { - if (!g_current_window) return; + if (!g_main_window) return; NSMenuItem *item = (NSMenuItem *)sender; - g_current_window->pending_menu_cmd = (S32)[item tag]; + g_main_window->pending_menu_cmd = (S32)[item tag]; } @end @@ -276,10 +259,17 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) { 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:(NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | - NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable) + styleMask:style_mask backing:NSBackingStoreBuffered defer:NO]; @@ -306,7 +296,19 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) { window->width = (S32)(desc->width * window->backing_scale); window->height = (S32)(desc->height * window->backing_scale); - g_current_window = window; + // 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]; @@ -317,9 +319,15 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) { 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_current_window == window) - g_current_window = nullptr; + if (g_main_window == window) + g_main_window = nullptr; delete window; } @@ -436,6 +444,10 @@ PlatformInput platform_get_input(PlatformWindow *window) { 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 @@ -474,3 +486,38 @@ const char *platform_clipboard_get() { 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; + } +} diff --git a/src/platform/platform_win32.cpp b/src/platform/platform_win32.cpp index 09f541c..eae8c5c 100644 --- a/src/platform/platform_win32.cpp +++ b/src/platform/platform_win32.cpp @@ -18,33 +18,37 @@ struct PlatformWindow { B32 prev_mouse_down; }; -static PlatformWindow *g_current_window = nullptr; +// Main window receives menu commands +static PlatformWindow *g_main_window = nullptr; static HCURSOR g_current_cursor = nullptr; +static B32 g_wndclass_registered = false; static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { + PlatformWindow *pw = (PlatformWindow *)GetWindowLongPtr(hwnd, GWLP_USERDATA); + switch (msg) { case WM_SIZE: - if (g_current_window && wparam != SIZE_MINIMIZED) { - g_current_window->width = (S32)LOWORD(lparam); - g_current_window->height = (S32)HIWORD(lparam); + if (pw && wparam != SIZE_MINIMIZED) { + pw->width = (S32)LOWORD(lparam); + pw->height = (S32)HIWORD(lparam); // Render a frame during the modal resize loop so the UI // stays responsive instead of showing a stretched image. - if (g_current_window->frame_callback) { - g_current_window->frame_callback(g_current_window->frame_callback_user_data); + if (pw->frame_callback) { + pw->frame_callback(pw->frame_callback_user_data); } } return 0; case WM_CHAR: - if (g_current_window && wparam >= 32 && wparam < 0xFFFF) { - PlatformInput *ev = &g_current_window->input; + if (pw && wparam >= 32 && wparam < 0xFFFF) { + PlatformInput *ev = &pw->input; if (ev->char_count < PLATFORM_MAX_CHARS_PER_FRAME) ev->chars[ev->char_count++] = (U16)wparam; } return 0; case WM_KEYDOWN: case WM_SYSKEYDOWN: - if (g_current_window) { - PlatformInput *ev = &g_current_window->input; + if (pw) { + PlatformInput *ev = &pw->input; if (ev->key_count < PLATFORM_MAX_KEYS_PER_FRAME) ev->keys[ev->key_count++] = (U8)wparam; ev->ctrl_held = (GetKeyState(VK_CONTROL) & 0x8000) != 0; @@ -52,14 +56,15 @@ static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM } break; // fall through to DefWindowProc for system keys case WM_MOUSEWHEEL: - if (g_current_window) { + if (pw) { S16 wheel_delta = (S16)HIWORD(wparam); - g_current_window->input.scroll_delta.y += (F32)wheel_delta / (F32)WHEEL_DELTA * 6.0f; + pw->input.scroll_delta.y += (F32)wheel_delta / (F32)WHEEL_DELTA * 6.0f; } return 0; case WM_COMMAND: - if (g_current_window && HIWORD(wparam) == 0) - g_current_window->pending_menu_cmd = (S32)LOWORD(wparam); + // Route menu commands to main window + if (g_main_window && HIWORD(wparam) == 0) + g_main_window->pending_menu_cmd = (S32)LOWORD(wparam); return 0; case WM_SETCURSOR: // When the cursor is in our client area, use the app-set cursor. @@ -69,7 +74,7 @@ static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM } break; case WM_DPICHANGED: - if (g_current_window) { + if (pw) { RECT *suggested = (RECT *)lparam; SetWindowPos(hwnd, nullptr, suggested->left, suggested->top, suggested->right - suggested->left, suggested->bottom - suggested->top, @@ -77,8 +82,8 @@ static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM } return 0; case WM_CLOSE: - if (g_current_window) - g_current_window->should_close = true; + if (pw) + pw->should_close = true; return 0; case WM_DESTROY: PostQuitMessage(0); @@ -94,14 +99,17 @@ static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM PlatformWindow *platform_create_window(PlatformWindowDesc *desc) { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); - WNDCLASSEXW wc = {}; - wc.cbSize = sizeof(wc); - wc.style = CS_CLASSDC; - wc.lpfnWndProc = win32_wndproc; - wc.hInstance = GetModuleHandleW(nullptr); - wc.hCursor = LoadCursor(nullptr, IDC_ARROW); - wc.lpszClassName = L"autosample_wc"; - RegisterClassExW(&wc); + if (!g_wndclass_registered) { + WNDCLASSEXW wc = {}; + wc.cbSize = sizeof(wc); + wc.style = CS_CLASSDC; + wc.lpfnWndProc = win32_wndproc; + wc.hInstance = GetModuleHandleW(nullptr); + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.lpszClassName = L"autosample_wc"; + RegisterClassExW(&wc); + g_wndclass_registered = true; + } UINT dpi = GetDpiForSystem(); int screen_w = GetSystemMetrics(SM_CXSCREEN); @@ -109,20 +117,29 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) { int x = (screen_w - desc->width) / 2; int y = (screen_h - desc->height) / 2; + DWORD style; + HWND parent_hwnd = nullptr; + if (desc->style == PLATFORM_WINDOW_STYLE_POPUP) { + style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU; + if (desc->parent) parent_hwnd = desc->parent->hwnd; + } else { + style = WS_OVERLAPPEDWINDOW; + } + RECT rect = { 0, 0, (LONG)desc->width, (LONG)desc->height }; - AdjustWindowRectExForDpi(&rect, WS_OVERLAPPEDWINDOW, FALSE, 0, dpi); + AdjustWindowRectExForDpi(&rect, style, FALSE, 0, dpi); int wchar_count = MultiByteToWideChar(CP_UTF8, 0, desc->title, -1, nullptr, 0); wchar_t *wtitle = (wchar_t *)_malloca(wchar_count * sizeof(wchar_t)); MultiByteToWideChar(CP_UTF8, 0, desc->title, -1, wtitle, wchar_count); HWND hwnd = CreateWindowExW( - 0, wc.lpszClassName, wtitle, - WS_OVERLAPPEDWINDOW, + 0, L"autosample_wc", wtitle, + style, x, y, rect.right - rect.left, rect.bottom - rect.top, - nullptr, nullptr, wc.hInstance, nullptr + parent_hwnd, nullptr, GetModuleHandleW(nullptr), nullptr ); _freea(wtitle); @@ -130,16 +147,23 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) { if (!hwnd) return nullptr; - ShowWindow(hwnd, SW_SHOWDEFAULT); - UpdateWindow(hwnd); - PlatformWindow *window = new PlatformWindow(); window->hwnd = hwnd; window->should_close = false; window->width = desc->width; window->height = desc->height; - g_current_window = window; + // Store PlatformWindow* on the HWND so WndProc can find it + SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)window); + + // Track main window for menu commands + if (desc->style == PLATFORM_WINDOW_STYLE_NORMAL) { + g_main_window = window; + } + + ShowWindow(hwnd, SW_SHOWDEFAULT); + UpdateWindow(hwnd); + return window; } @@ -148,11 +172,10 @@ void platform_destroy_window(PlatformWindow *window) { if (window->hwnd) { DestroyWindow(window->hwnd); - UnregisterClassW(L"autosample_wc", GetModuleHandleW(nullptr)); } - if (g_current_window == window) - g_current_window = nullptr; + if (g_main_window == window) + g_main_window = nullptr; delete window; } @@ -237,6 +260,10 @@ PlatformInput platform_get_input(PlatformWindow *window) { return result; } +B32 platform_window_should_close(PlatformWindow *window) { + return window ? window->should_close : 0; +} + F32 platform_get_dpi_scale(PlatformWindow *window) { if (!window || !window->hwnd) return 1.0f; return (F32)GetDpiForWindow(window->hwnd) / 96.0f; @@ -267,7 +294,7 @@ void platform_clipboard_set(const char *text) { wbuf[wlen] = L'\0'; GlobalUnlock(hmem); - HWND hwnd = g_current_window ? g_current_window->hwnd : nullptr; + HWND hwnd = g_main_window ? g_main_window->hwnd : nullptr; if (OpenClipboard(hwnd)) { EmptyClipboard(); SetClipboardData(CF_UNICODETEXT, hmem); @@ -281,7 +308,7 @@ const char *platform_clipboard_get() { static char buf[4096]; buf[0] = '\0'; - HWND hwnd = g_current_window ? g_current_window->hwnd : nullptr; + HWND hwnd = g_main_window ? g_main_window->hwnd : nullptr; if (!OpenClipboard(hwnd)) return nullptr; HGLOBAL hmem = GetClipboardData(CF_UNICODETEXT); @@ -297,3 +324,37 @@ const char *platform_clipboard_get() { CloseClipboard(); return buf[0] ? buf : nullptr; } + +S32 platform_message_box(PlatformWindow *parent, const char *title, + const char *message, PlatformMsgBoxType type) { + UINT mb_type; + switch (type) { + case PLATFORM_MSGBOX_OK: mb_type = MB_OK; break; + case PLATFORM_MSGBOX_OK_CANCEL: mb_type = MB_OKCANCEL; break; + case PLATFORM_MSGBOX_YES_NO: mb_type = MB_YESNO; break; + default: mb_type = MB_OK; break; + } + + // Convert UTF-8 to wide strings + int title_wlen = MultiByteToWideChar(CP_UTF8, 0, title, -1, nullptr, 0); + wchar_t *wtitle = (wchar_t *)_malloca(title_wlen * sizeof(wchar_t)); + MultiByteToWideChar(CP_UTF8, 0, title, -1, wtitle, title_wlen); + + int msg_wlen = MultiByteToWideChar(CP_UTF8, 0, message, -1, nullptr, 0); + wchar_t *wmsg = (wchar_t *)_malloca(msg_wlen * sizeof(wchar_t)); + MultiByteToWideChar(CP_UTF8, 0, message, -1, wmsg, msg_wlen); + + HWND hwnd = parent ? parent->hwnd : nullptr; + int result = MessageBoxW(hwnd, wmsg, wtitle, mb_type); + + _freea(wmsg); + _freea(wtitle); + + switch (result) { + case IDOK: return 0; + case IDYES: return 0; + case IDCANCEL: return 1; + case IDNO: return 1; + default: return -1; + } +} diff --git a/src/renderer/renderer.h b/src/renderer/renderer.h index f964591..7681f97 100644 --- a/src/renderer/renderer.h +++ b/src/renderer/renderer.h @@ -13,6 +13,13 @@ struct RendererDesc { }; Renderer *renderer_create(RendererDesc *desc); + +// creates a lightweight renderer that shares the GPU device, pipeline, shaders, font atlas, and icon textures from a +// parent renderer, but creates its own drawable surface (CAMetalLayer / swap chain), vertex/index buffers, and frame synchronization +// primitives. On each renderer_begin_frame(), shared renderers sync font atlas updates from the parent so Cmd+/- zoom works across all +// windows. +Renderer *renderer_create_shared(Renderer *parent, RendererDesc *desc); + void renderer_destroy(Renderer *renderer); B32 renderer_begin_frame(Renderer *renderer); void renderer_end_frame(Renderer *renderer, Clay_RenderCommandArray render_commands); diff --git a/src/renderer/renderer_dx12.cpp b/src/renderer/renderer_dx12.cpp index 8a224f5..76317e5 100644 --- a/src/renderer/renderer_dx12.cpp +++ b/src/renderer/renderer_dx12.cpp @@ -178,6 +178,7 @@ struct FrameContext { // Renderer struct struct Renderer { + Renderer *parent; // non-null for shared renderers HWND hwnd; S32 width; S32 height; @@ -1063,23 +1064,78 @@ fail: return nullptr; } +Renderer *renderer_create_shared(Renderer *parent, RendererDesc *desc) { + if (!parent) return nullptr; + + Renderer *r = new Renderer(); + memset(r, 0, sizeof(*r)); + + r->parent = parent; + r->hwnd = (HWND)desc->window_handle; + r->width = desc->width; + r->height = desc->height; + r->frame_count = desc->frame_count; + if (r->frame_count > NUM_BACK_BUFFERS) r->frame_count = NUM_BACK_BUFFERS; + + // Share from parent (not ref-counted — parent must outlive children) + r->device = parent->device; + r->command_queue = parent->command_queue; + r->root_signature = parent->root_signature; + r->pipeline_state = parent->pipeline_state; + r->font_texture = parent->font_texture; + r->icon_texture = parent->icon_texture; + r->icon_srv_heap = parent->icon_srv_heap; + r->srv_heap = parent->srv_heap; + r->ft_lib = parent->ft_lib; + r->ft_face = parent->ft_face; + memcpy(r->glyphs, parent->glyphs, sizeof(r->glyphs)); + r->font_atlas_size = parent->font_atlas_size; + r->font_line_height = parent->font_line_height; + + // Create own RTV heap (we don't need own SRV heap — use parent's) + { + D3D12_DESCRIPTOR_HEAP_DESC rtv_desc = {}; + rtv_desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV; + rtv_desc.NumDescriptors = NUM_BACK_BUFFERS; + rtv_desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE; + rtv_desc.NodeMask = 1; + if (r->device->CreateDescriptorHeap(&rtv_desc, IID_PPV_ARGS(&r->rtv_heap)) != S_OK) + goto fail; + + SIZE_T rtv_size = r->device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); + D3D12_CPU_DESCRIPTOR_HANDLE handle = r->rtv_heap->GetCPUDescriptorHandleForHeapStart(); + for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { + r->rtv_descriptors[i] = handle; + handle.ptr += rtv_size; + } + } + + if (!create_frame_resources(r)) goto fail; + if (!create_swap_chain(r)) goto fail; + create_render_targets(r); + if (!create_ui_buffers(r)) goto fail; + + r->clear_r = parent->clear_r; + r->clear_g = parent->clear_g; + r->clear_b = parent->clear_b; + + return r; + +fail: + renderer_destroy(r); + return nullptr; +} + void renderer_destroy(Renderer *r) { if (!r) return; wait_for_pending(r); + // Per-window resources (always owned) for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { if (r->vertex_buffers[i]) r->vertex_buffers[i]->Release(); if (r->index_buffers[i]) r->index_buffers[i]->Release(); } - if (r->font_texture) r->font_texture->Release(); - if (r->icon_texture) r->icon_texture->Release(); - if (r->icon_srv_heap) r->icon_srv_heap->Release(); - if (r->pipeline_state) r->pipeline_state->Release(); - if (r->root_signature) r->root_signature->Release(); - - if (r->ft_face) FT_Done_Face(r->ft_face); - if (r->ft_lib) FT_Done_FreeType(r->ft_lib); cleanup_render_targets(r); @@ -1087,26 +1143,49 @@ void renderer_destroy(Renderer *r) { if (r->swap_chain_waitable) CloseHandle(r->swap_chain_waitable); for (S32 i = 0; i < r->frame_count; i++) if (r->frames[i].command_allocator) r->frames[i].command_allocator->Release(); - if (r->command_queue) r->command_queue->Release(); if (r->command_list) r->command_list->Release(); if (r->rtv_heap) r->rtv_heap->Release(); - if (r->srv_heap) r->srv_heap->Release(); if (r->fence) r->fence->Release(); if (r->fence_event) CloseHandle(r->fence_event); - if (r->device) r->device->Release(); + + // Shared resources (only freed by root renderer) + if (!r->parent) { + if (r->font_texture) r->font_texture->Release(); + if (r->icon_texture) r->icon_texture->Release(); + if (r->icon_srv_heap) r->icon_srv_heap->Release(); + if (r->pipeline_state) r->pipeline_state->Release(); + if (r->root_signature) r->root_signature->Release(); + if (r->srv_heap) r->srv_heap->Release(); + if (r->command_queue) r->command_queue->Release(); + if (r->device) r->device->Release(); + + if (r->ft_face) FT_Done_Face(r->ft_face); + if (r->ft_lib) FT_Done_FreeType(r->ft_lib); #ifdef DX12_ENABLE_DEBUG_LAYER - IDXGIDebug1 *dxgi_debug = nullptr; - if (SUCCEEDED(DXGIGetDebugInterface1(0, IID_PPV_ARGS(&dxgi_debug)))) { - dxgi_debug->ReportLiveObjects(DXGI_DEBUG_ALL, DXGI_DEBUG_RLO_SUMMARY); - dxgi_debug->Release(); - } + IDXGIDebug1 *dxgi_debug = nullptr; + if (SUCCEEDED(DXGIGetDebugInterface1(0, IID_PPV_ARGS(&dxgi_debug)))) { + dxgi_debug->ReportLiveObjects(DXGI_DEBUG_ALL, DXGI_DEBUG_RLO_SUMMARY); + dxgi_debug->Release(); + } #endif + } delete r; } B32 renderer_begin_frame(Renderer *r) { + // Sync shared resources from parent (font atlas may have been rebuilt) + if (r->parent) { + r->font_texture = r->parent->font_texture; + r->icon_texture = r->parent->icon_texture; + r->icon_srv_heap = r->parent->icon_srv_heap; + r->srv_heap = r->parent->srv_heap; + memcpy(r->glyphs, r->parent->glyphs, sizeof(r->glyphs)); + r->font_atlas_size = r->parent->font_atlas_size; + r->font_line_height = r->parent->font_line_height; + } + if ((r->swap_chain_occluded && r->swap_chain->Present(0, DXGI_PRESENT_TEST) == DXGI_STATUS_OCCLUDED) || IsIconic(r->hwnd)) { diff --git a/src/renderer/renderer_metal.mm b/src/renderer/renderer_metal.mm index c97c31c..d25836a 100644 --- a/src/renderer/renderer_metal.mm +++ b/src/renderer/renderer_metal.mm @@ -153,6 +153,7 @@ fragment float4 fragment_main(Fragment in [[stage_in]], // Renderer struct struct Renderer { + Renderer *parent; // non-null for shared renderers S32 width; S32 height; S32 frame_count; @@ -641,16 +642,91 @@ Renderer *renderer_create(RendererDesc *desc) { return r; } +Renderer *renderer_create_shared(Renderer *parent, RendererDesc *desc) { + if (!parent) return nullptr; + + Renderer *r = new Renderer(); + memset(r, 0, sizeof(*r)); + + r->parent = parent; + r->width = desc->width; + r->height = desc->height; + r->frame_count = desc->frame_count; + if (r->frame_count > NUM_BACK_BUFFERS) r->frame_count = NUM_BACK_BUFFERS; + + // Share from parent + r->device = parent->device; + r->command_queue = parent->command_queue; + r->pipeline_state = parent->pipeline_state; + r->font_texture = parent->font_texture; + r->font_sampler = parent->font_sampler; + r->icon_texture = parent->icon_texture; + r->ft_lib = parent->ft_lib; + r->ft_face = parent->ft_face; + memcpy(r->glyphs, parent->glyphs, sizeof(r->glyphs)); + r->font_atlas_size = parent->font_atlas_size; + r->font_line_height = parent->font_line_height; + + // Create own metal layer for the popup's NSView + NSView *view = (__bridge NSView *)desc->window_handle; + [view setWantsLayer:YES]; + + CAMetalLayer *layer = [CAMetalLayer layer]; + layer.device = r->device; + layer.pixelFormat = MTLPixelFormatBGRA8Unorm; + layer.framebufferOnly = YES; + layer.displaySyncEnabled = YES; + layer.maximumDrawableCount = 3; + + NSWindow *window = [view window]; + r->backing_scale = (F32)[window backingScaleFactor]; + layer.contentsScale = r->backing_scale; + layer.drawableSize = CGSizeMake(r->width, r->height); + + [view setLayer:layer]; + r->metal_layer = layer; + + r->frame_semaphore = dispatch_semaphore_create(NUM_BACK_BUFFERS); + + // Create own vertex/index buffers + for (S32 i = 0; i < NUM_BACK_BUFFERS; i++) { + r->vertex_buffers[i] = [r->device newBufferWithLength:MAX_VERTICES * sizeof(UIVertex) + options:MTLResourceStorageModeShared]; + r->index_buffers[i] = [r->device newBufferWithLength:MAX_INDICES * sizeof(U32) + options:MTLResourceStorageModeShared]; + } + + // Default clear color + r->clear_r = parent->clear_r; + r->clear_g = parent->clear_g; + r->clear_b = parent->clear_b; + + return r; +} + void renderer_destroy(Renderer *r) { if (!r) return; - if (r->ft_face) FT_Done_Face(r->ft_face); - if (r->ft_lib) FT_Done_FreeType(r->ft_lib); + // Only free FreeType resources if we own them (not shared) + if (!r->parent) { + if (r->ft_face) FT_Done_Face(r->ft_face); + if (r->ft_lib) FT_Done_FreeType(r->ft_lib); + } delete r; } B32 renderer_begin_frame(Renderer *r) { if (r->width <= 0 || r->height <= 0) return false; + // Sync shared resources from parent (font atlas may have been rebuilt) + if (r->parent) { + r->font_texture = r->parent->font_texture; + r->font_sampler = r->parent->font_sampler; + r->icon_texture = r->parent->icon_texture; + memcpy(r->glyphs, r->parent->glyphs, sizeof(r->glyphs)); + r->font_atlas_size = r->parent->font_atlas_size; + r->font_line_height = r->parent->font_line_height; + } + // Wait for an in-flight frame to finish, then acquire a drawable. // Doing this BEFORE input sampling ensures the freshest mouse position // is used for rendering, reducing perceived drag latency. diff --git a/src/ui/ui_core.h b/src/ui/ui_core.h index 8052480..15d2b48 100644 --- a/src/ui/ui_core.h +++ b/src/ui/ui_core.h @@ -183,13 +183,6 @@ struct CustomRotatedIconData { #define CORNER_RADIUS uis(g_theme.corner_radius) -//////////////////////////////// -// Modal / window styling - -#define MODAL_OVERLAY_COLOR Clay_Color{ 0, 0, 0, 120} -#define MODAL_WIDTH uis(400) -#define WINDOW_TITLE_HEIGHT uis(32) - //////////////////////////////// // Panel sizing diff --git a/src/ui/ui_piano.cpp b/src/ui/ui_piano.cpp new file mode 100644 index 0000000..7354348 --- /dev/null +++ b/src/ui/ui_piano.cpp @@ -0,0 +1,136 @@ +#include "ui/ui_piano.h" + +#define PIANO_BLACK_W 11.0f +#define PIANO_BLACK_H_PCT 0.6f + +B32 piano_is_black_key(S32 note) { + S32 n = note % 12; + return n == 1 || n == 3 || n == 6 || n == 8 || n == 10; +} + +// Velocity-based color: blue (vel 0) → green (mid) → red (vel 127) +Clay_Color piano_velocity_color(S32 velocity) { + F32 t = (F32)velocity / 127.0f; + F32 r, g, b; + if (t < 0.5f) { + F32 s = t * 2.0f; + r = 40.0f + s * (76.0f - 40.0f); + g = 120.0f + s * (175.0f - 120.0f); + b = 220.0f + s * (80.0f - 220.0f); + } else { + F32 s = (t - 0.5f) * 2.0f; + r = 76.0f + s * (220.0f - 76.0f); + g = 175.0f + s * (50.0f - 175.0f); + b = 80.0f + s * (40.0f - 80.0f); + } + return Clay_Color{r, g, b, 255}; +} + +void ui_piano(UI_PianoState *state, MidiEngine *midi, F32 avail_w, F32 avail_h) { + Clay_ElementId piano_id = CLAY_ID("PianoContainer"); + CLAY(piano_id, + .layout = { + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, + .layoutDirection = CLAY_LEFT_TO_RIGHT, + } + ) { + // Compute black key size proportional to white keys + F32 black_key_h = avail_h * PIANO_BLACK_H_PCT; + if (black_key_h < uis(20)) black_key_h = uis(20); + + F32 white_key_w = avail_w / 52.0f; + F32 black_key_w = white_key_w * 0.6f; + if (black_key_w < uis(8)) black_key_w = uis(8); + + // White keys (grow to fill width and height) + for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) { + if (piano_is_black_key(note)) continue; + + B32 midi_held = midi_is_note_held(midi, note); + B32 mouse_held = state->mouse_note == note; + Clay_Color bg; + if (midi_held) { + bg = piano_velocity_color(midi_get_note_velocity(midi, note)); + } else if (mouse_held) { + bg = g_theme.accent; + } else { + bg = Clay_Color{240, 240, 240, 255}; + } + + CLAY(CLAY_IDI("PKey", note), + .layout = { + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, + }, + .backgroundColor = bg, + .border = { .color = {190, 190, 190, 255}, .width = { .right = 1 } }, + ) {} + } + + // Black keys (floating, attached to left white key) + for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) { + if (!piano_is_black_key(note)) continue; + + Clay_ElementId parent_wkey = CLAY_IDI("PKey", note - 1); + B32 midi_held = midi_is_note_held(midi, note); + B32 mouse_held = state->mouse_note == note; + Clay_Color bg; + if (midi_held) { + bg = piano_velocity_color(midi_get_note_velocity(midi, note)); + } else if (mouse_held) { + bg = g_theme.accent; + } else { + bg = Clay_Color{25, 25, 30, 255}; + } + + CLAY(CLAY_IDI("PKey", note), + .layout = { + .sizing = { + .width = CLAY_SIZING_FIXED(black_key_w), + .height = CLAY_SIZING_FIXED(black_key_h), + }, + }, + .backgroundColor = bg, + .cornerRadius = { .topLeft = 0, .topRight = 0, .bottomLeft = uis(2), .bottomRight = uis(2) }, + .floating = { + .parentId = parent_wkey.id, + .zIndex = 100, + .attachPoints = { + .element = CLAY_ATTACH_POINT_CENTER_TOP, + .parent = CLAY_ATTACH_POINT_RIGHT_TOP, + }, + .attachTo = CLAY_ATTACH_TO_ELEMENT_WITH_ID, + }, + ) {} + } + } +} + +void ui_piano_update_input(UI_PianoState *state) { + PlatformInput input = g_wstate.input; + + if (!input.mouse_down) { + state->mouse_note = -1; + return; + } + + // Find hovered piano key — check black keys first (they're on top) + S32 hovered_note = -1; + for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) { + if (piano_is_black_key(note) && Clay_PointerOver(CLAY_IDI("PKey", note))) { + hovered_note = note; + break; + } + } + if (hovered_note == -1) { + for (S32 note = PIANO_FIRST_NOTE; note <= PIANO_LAST_NOTE; note++) { + if (!piano_is_black_key(note) && Clay_PointerOver(CLAY_IDI("PKey", note))) { + hovered_note = note; + break; + } + } + } + + if (hovered_note != -1) { + state->mouse_note = hovered_note; + } +} diff --git a/src/ui/ui_piano.h b/src/ui/ui_piano.h new file mode 100644 index 0000000..d883365 --- /dev/null +++ b/src/ui/ui_piano.h @@ -0,0 +1,20 @@ +#pragma once +#include "ui/ui_core.h" +#include "midi/midi.h" + +#define PIANO_FIRST_NOTE 21 // A0 +#define PIANO_LAST_NOTE 108 // C8 + +struct UI_PianoState { + S32 mouse_note; // MIDI note held by mouse click (-1 = none) +}; + +B32 piano_is_black_key(S32 note); +Clay_Color piano_velocity_color(S32 velocity); + +// Render the 88-key piano widget. +// avail_w: total width for key layout, avail_h: total height for keys +void ui_piano(UI_PianoState *state, MidiEngine *midi, F32 avail_w, F32 avail_h); + +// Update piano mouse input (call after Clay layout is computed) +void ui_piano_update_input(UI_PianoState *state); diff --git a/src/ui/ui_popups.cpp b/src/ui/ui_popups.cpp new file mode 100644 index 0000000..131e6aa --- /dev/null +++ b/src/ui/ui_popups.cpp @@ -0,0 +1,143 @@ +#include "ui/ui_popups.h" + +static PopupWindow g_popups[MAX_POPUP_WINDOWS]; + +PopupWindow *popup_find_by_flag(B32 *flag) { + for (S32 i = 0; i < MAX_POPUP_WINDOWS; i++) + if (g_popups[i].alive && g_popups[i].open_flag == flag) + return &g_popups[i]; + return nullptr; +} + +PopupWindow *popup_open(PlatformWindow *parent_window, Renderer *parent_renderer, + const char *title, B32 *open_flag, + S32 width, S32 height, + UI_WindowContentFn content_fn, void *user_data) { + // Find free slot + PopupWindow *popup = nullptr; + for (S32 i = 0; i < MAX_POPUP_WINDOWS; i++) { + if (!g_popups[i].alive) { popup = &g_popups[i]; break; } + } + if (!popup) return nullptr; + + memset(popup, 0, sizeof(*popup)); + + // Create native popup window + PlatformWindowDesc desc = {}; + desc.title = title; + desc.width = width; + desc.height = height; + desc.style = PLATFORM_WINDOW_STYLE_POPUP; + desc.parent = parent_window; + popup->platform_window = platform_create_window(&desc); + if (!popup->platform_window) return nullptr; + + // Create shared renderer + S32 pw, ph; + platform_get_size(popup->platform_window, &pw, &ph); + + RendererDesc rdesc = {}; + rdesc.window_handle = platform_get_native_handle(popup->platform_window); + rdesc.width = pw; + rdesc.height = ph; + popup->renderer = renderer_create_shared(parent_renderer, &rdesc); + if (!popup->renderer) { + platform_destroy_window(popup->platform_window); + return nullptr; + } + + // Create UI context + popup->ui_ctx = ui_create((F32)pw, (F32)ph); + ui_set_measure_text_fn(popup->ui_ctx, renderer_measure_text, popup->renderer); + + popup->alive = 1; + popup->open_flag = open_flag; + popup->content_fn = content_fn; + popup->content_user_data = user_data; + popup->width = width; + popup->height = height; + popup->last_w = pw; + popup->last_h = ph; + popup->title = title; + popup->wstate = {}; + + return popup; +} + +void popup_close(PopupWindow *popup) { + if (!popup || !popup->alive) return; + + if (popup->open_flag) + *popup->open_flag = 0; + + ui_destroy(popup->ui_ctx); + renderer_destroy(popup->renderer); + platform_destroy_window(popup->platform_window); + + popup->alive = 0; +} + +void popup_do_frame(PopupWindow *popup, F32 dt) { + if (!popup->alive) return; + + // Check window size + S32 w, h; + platform_get_size(popup->platform_window, &w, &h); + if (w != popup->last_w || h != popup->last_h) { + renderer_resize(popup->renderer, w, h); + popup->last_w = w; + popup->last_h = h; + } + + if (!renderer_begin_frame(popup->renderer)) + return; + + // Gather input from popup window + PlatformInput input = platform_get_input(popup->platform_window); + + // Swap widget state + UI_WidgetState saved_wstate = g_wstate; + g_wstate = popup->wstate; + + ui_widgets_begin_frame(input); + + // Build UI + ui_begin_frame(popup->ui_ctx, (F32)w, (F32)h, input.mouse_pos, input.mouse_down, + input.scroll_delta, dt); + + // Background fill + CLAY(CLAY_ID("PopupBg"), + .layout = { + .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, + .padding = { uip(12), uip(12), uip(10), uip(10) }, + .childGap = uip(8), + .layoutDirection = CLAY_TOP_TO_BOTTOM, + }, + .backgroundColor = g_theme.bg_medium, + ) { + if (popup->content_fn) { + popup->content_fn(popup->content_user_data); + } + } + + Clay_RenderCommandArray render_commands = ui_end_frame(popup->ui_ctx); + + // Save widget state back + popup->wstate = g_wstate; + g_wstate = saved_wstate; + + // Render + renderer_end_frame(popup->renderer, render_commands); +} + +void popup_close_all() { + for (S32 i = 0; i < MAX_POPUP_WINDOWS; i++) + if (g_popups[i].alive) popup_close(&g_popups[i]); +} + +void popup_close_check() { + for (S32 i = 0; i < MAX_POPUP_WINDOWS; i++) { + if (g_popups[i].alive && platform_window_should_close(g_popups[i].platform_window)) + popup_close(&g_popups[i]); + } +} diff --git a/src/ui/ui_popups.h b/src/ui/ui_popups.h new file mode 100644 index 0000000..8f96f3e --- /dev/null +++ b/src/ui/ui_popups.h @@ -0,0 +1,31 @@ +#pragma once +#include "ui/ui_core.h" +#include "ui/ui_widgets.h" +#include "platform/platform.h" +#include "renderer/renderer.h" + +#define MAX_POPUP_WINDOWS 4 + +struct PopupWindow { + B32 alive; + B32 *open_flag; // e.g. &app->show_settings_window + PlatformWindow *platform_window; + Renderer *renderer; + UI_Context *ui_ctx; + UI_WidgetState wstate; + UI_WindowContentFn content_fn; + void *content_user_data; + S32 width, height; + S32 last_w, last_h; + const char *title; +}; + +PopupWindow *popup_open(PlatformWindow *parent_window, Renderer *parent_renderer, + const char *title, B32 *open_flag, + S32 width, S32 height, + UI_WindowContentFn content_fn, void *user_data); +void popup_close(PopupWindow *popup); +PopupWindow *popup_find_by_flag(B32 *flag); +void popup_do_frame(PopupWindow *popup, F32 dt); +void popup_close_all(void); +void popup_close_check(void); diff --git a/src/ui/ui_widgets.cpp b/src/ui/ui_widgets.cpp index 0920237..7444758 100644 --- a/src/ui/ui_widgets.cpp +++ b/src/ui/ui_widgets.cpp @@ -149,11 +149,6 @@ void ui_widgets_begin_frame(PlatformInput input) { } } - // Drag cleanup: if mouse was released between frames, end any active drag - if (g_wstate.drag.dragging_id != 0 && !input.mouse_down) { - g_wstate.drag.dragging_id = 0; - } - ui_text_input_reset_display_bufs(); } @@ -971,358 +966,6 @@ B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected) return changed; } -//////////////////////////////// -// Modal dialog - -B32 ui_modal_is_active() { - return g_wstate.modal.active; -} - -S32 ui_modal(const char *id, const char *title, const char *message, - const char **buttons, S32 button_count) { - ensure_widget_text_configs(); - - Clay_ElementId eid = WID(id); - - // First call activates the modal - if (!g_wstate.modal.active) { - g_wstate.modal.active = 1; - g_wstate.modal.id = eid.id; - g_wstate.modal.result = -1; - } - - // If a different modal is active, ignore this one - if (g_wstate.modal.id != eid.id) return -1; - - S32 result = -1; - - // Check Escape key to dismiss - for (S32 k = 0; k < g_wstate.input.key_count; k++) { - if (g_wstate.input.keys[k] == PKEY_ESCAPE) { - result = -2; - break; - } - } - - // Full-screen overlay (dims background, captures all pointer events) - CLAY(WIDI(id, 2000), - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() }, - }, - .backgroundColor = Clay_Color{0, 0, 0, 120}, - .floating = { - .zIndex = 1000, - .attachPoints = { - .element = CLAY_ATTACH_POINT_LEFT_TOP, - .parent = CLAY_ATTACH_POINT_LEFT_TOP, - }, - .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_CAPTURE, - .attachTo = CLAY_ATTACH_TO_ROOT, - } - ) {} - - // Modal drop shadow - { - Clay_ElementId modal_box_id = WIDI(id, 2001); - Clay_BoundingBox modal_bb = Clay_GetElementData(modal_box_id).boundingBox; - emit_shadow(modal_bb, uis(3), uis(4), uis(10), - g_theme.shadow.a, 1000, - 1, 0, CLAY_ATTACH_POINT_LEFT_TOP); - } - - // Dialog box (centered) - CLAY(WIDI(id, 2001), - .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(MODAL_WIDTH), .height = CLAY_SIZING_FIT() }, - .layoutDirection = CLAY_TOP_TO_BOTTOM, - }, - .backgroundColor = g_theme.bg_medium, - .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), - .floating = { - .zIndex = 1001, - .attachPoints = { - .element = CLAY_ATTACH_POINT_CENTER_CENTER, - .parent = CLAY_ATTACH_POINT_CENTER_CENTER, - }, - .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_CAPTURE, - .attachTo = CLAY_ATTACH_TO_ROOT, - }, - .border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } } - ) { - // Title bar (gradient: lighter top) - { - Clay_Color mtb = g_theme.title_bar; - Clay_Color mtb_top = {(F32)Min((S32)mtb.r+12,255), (F32)Min((S32)mtb.g+12,255), (F32)Min((S32)mtb.b+12,255), mtb.a}; - CustomGradientData *mtb_grad = alloc_gradient(mtb_top, mtb); - - CLAY(WIDI(id, 2002), - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WINDOW_TITLE_HEIGHT) }, - .padding = { uip(12), uip(12), 0, 0 }, - .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, - }, - .backgroundColor = g_theme.title_bar, - .cornerRadius = { CORNER_RADIUS, CORNER_RADIUS, 0, 0 }, - .custom = { .customData = mtb_grad }, - .border = { .color = g_theme.border, .width = { .bottom = 1 } }, - ) { - CLAY_TEXT(clay_str(title), &g_widget_text_config); - } - } - - // Message body - static Clay_TextElementConfig msg_text_config; - msg_text_config = g_widget_text_config; - msg_text_config.wrapMode = CLAY_TEXT_WRAP_WORDS; - - CLAY(WIDI(id, 2003), - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .padding = { uip(16), uip(16), uip(16), uip(16) }, - } - ) { - CLAY_TEXT(clay_str(message), &msg_text_config); - } - - // Button row (right-aligned) - CLAY(WIDI(id, 2004), - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .padding = { uip(16), uip(16), uip(8), uip(16) }, - .childGap = uip(8), - .childAlignment = { .x = CLAY_ALIGN_X_RIGHT, .y = CLAY_ALIGN_Y_CENTER }, - .layoutDirection = CLAY_LEFT_TO_RIGHT, - } - ) { - for (S32 i = 0; i < button_count; i++) { - Clay_ElementId btn_id = WIDI(id, 2100 + i); - B32 btn_hovered = Clay_PointerOver(btn_id); - - Clay_Color mbtn_base = btn_hovered ? g_theme.accent_hover : g_theme.accent; - Clay_Color mbtn_top = {(F32)Min((S32)mbtn_base.r+12,255), (F32)Min((S32)mbtn_base.g+12,255), (F32)Min((S32)mbtn_base.b+12,255), mbtn_base.a}; - Clay_Color mbtn_bot = {(F32)Max((S32)mbtn_base.r-15,0), (F32)Max((S32)mbtn_base.g-15,0), (F32)Max((S32)mbtn_base.b-15,0), mbtn_base.a}; - CustomGradientData *mbtn_grad = alloc_gradient(mbtn_top, mbtn_bot); - - CLAY(btn_id, - .layout = { - .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(WIDGET_BUTTON_HEIGHT) }, - .padding = { uip(16), uip(16), uip(1), 0 }, - .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, - }, - .backgroundColor = mbtn_base, - .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), - .custom = { .customData = mbtn_grad }, - ) { - CLAY_TEXT(clay_str(buttons[i]), &g_widget_text_config_btn); - } - - if (btn_hovered && g_wstate.mouse_clicked) { - result = i; - } - } - } - } - - // Deactivate modal if a result was produced - if (result != -1) { - g_wstate.modal.active = 0; - g_wstate.modal.id = 0; - g_wstate.modal.result = result; - } - - return result; -} - -//////////////////////////////// -// Draggable window - -static UI_WindowSlot *find_or_create_window_slot(U32 id, Vec2F32 initial_pos, Vec2F32 initial_size) { - // Look for existing slot - for (S32 i = 0; i < g_wstate.window_count; i++) { - if (g_wstate.windows[i].id == id) { - return &g_wstate.windows[i]; - } - } - // Create new slot - if (g_wstate.window_count >= UI_WIDGET_MAX_WINDOWS) return nullptr; - UI_WindowSlot *slot = &g_wstate.windows[g_wstate.window_count++]; - slot->id = id; - slot->position = initial_pos; - slot->size = initial_size; - slot->open = 1; - slot->z_order = g_wstate.next_z++; - return slot; -} - -static void bring_window_to_front(UI_WindowSlot *slot) { - // Renormalize if approaching modal z-range - if (g_wstate.next_z > 800) { - S16 sorted[UI_WIDGET_MAX_WINDOWS]; - S32 count = g_wstate.window_count; - for (S32 i = 0; i < count; i++) sorted[i] = g_wstate.windows[i].z_order; - // Bubble sort (tiny array) - for (S32 i = 0; i < count - 1; i++) { - for (S32 j = i + 1; j < count; j++) { - if (sorted[j] < sorted[i]) { - S16 tmp = sorted[i]; sorted[i] = sorted[j]; sorted[j] = tmp; - } - } - } - for (S32 i = 0; i < count; i++) { - for (S32 j = 0; j < count; j++) { - if (g_wstate.windows[j].z_order == sorted[i]) { - g_wstate.windows[j].z_order = (S16)i; - } - } - } - g_wstate.next_z = (S16)count; - } - slot->z_order = g_wstate.next_z++; -} - -B32 ui_window(const char *id, const char *title, B32 *open, - Vec2F32 initial_pos, Vec2F32 initial_size, - UI_WindowContentFn content_fn, void *user_data) { - ensure_widget_text_configs(); - - if (!*open) return 0; - - Clay_ElementId eid = WID(id); - UI_WindowSlot *slot = find_or_create_window_slot(eid.id, initial_pos, initial_size); - if (!slot) return 0; - - // Drag handling - Clay_ElementId title_bar_id = WIDI(id, 3000); - B32 title_hovered = Clay_PointerOver(title_bar_id); - - // Start drag on title bar click - if (title_hovered && g_wstate.mouse_clicked && g_wstate.drag.dragging_id == 0) { - g_wstate.drag.dragging_id = eid.id; - g_wstate.drag.drag_anchor = g_wstate.input.mouse_pos; - g_wstate.drag.pos_anchor = slot->position; - bring_window_to_front(slot); - } - - // Continue drag - if (g_wstate.drag.dragging_id == eid.id && g_wstate.input.mouse_down) { - Vec2F32 delta; - delta.x = g_wstate.input.mouse_pos.x - g_wstate.drag.drag_anchor.x; - delta.y = g_wstate.input.mouse_pos.y - g_wstate.drag.drag_anchor.y; - slot->position.x = g_wstate.drag.pos_anchor.x + delta.x; - slot->position.y = g_wstate.drag.pos_anchor.y + delta.y; - } - - // End drag on release - if (g_wstate.drag.dragging_id == eid.id && !g_wstate.input.mouse_down) { - g_wstate.drag.dragging_id = 0; - } - - // Click anywhere on window body brings to front - B32 body_hovered = Clay_PointerOver(eid); - if (body_hovered && g_wstate.mouse_clicked && g_wstate.drag.dragging_id == 0) { - bring_window_to_front(slot); - } - - // Close button - Clay_ElementId close_id = WIDI(id, 3001); - B32 close_hovered = Clay_PointerOver(close_id); - - // Drop shadow - { - Clay_BoundingBox win_bb = Clay_GetElementData(eid).boundingBox; - // Use absolute position since window uses offset-based floating - Clay_BoundingBox shadow_bb = { slot->position.x, slot->position.y, win_bb.width, win_bb.height }; - emit_shadow(shadow_bb, uis(3), uis(3), uis(8), - g_theme.shadow.a, (S16)(100 + slot->z_order - 1), - 1, 0, CLAY_ATTACH_POINT_LEFT_TOP); - } - - // Window floating element - CLAY(eid, - .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(slot->size.x * g_ui_scale), .height = CLAY_SIZING_FIT() }, - .layoutDirection = CLAY_TOP_TO_BOTTOM, - }, - .backgroundColor = g_theme.bg_medium, - .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS), - .floating = { - .offset = { slot->position.x, slot->position.y }, - .zIndex = (S16)(100 + slot->z_order), - .attachPoints = { - .element = CLAY_ATTACH_POINT_LEFT_TOP, - .parent = CLAY_ATTACH_POINT_LEFT_TOP, - }, - .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_CAPTURE, - .attachTo = CLAY_ATTACH_TO_ROOT, - }, - .border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } } - ) { - // Title bar (gradient: lighter top) - Clay_Color tb = g_theme.title_bar; - Clay_Color tb_top = {(F32)Min((S32)tb.r+12,255), (F32)Min((S32)tb.g+12,255), (F32)Min((S32)tb.b+12,255), tb.a}; - CustomGradientData *tb_grad = alloc_gradient(tb_top, tb); - - CLAY(title_bar_id, - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(WINDOW_TITLE_HEIGHT) }, - .padding = { uip(10), uip(10), 0, 0 }, - .childAlignment = { .y = CLAY_ALIGN_Y_CENTER }, - .layoutDirection = CLAY_LEFT_TO_RIGHT, - }, - .backgroundColor = g_theme.title_bar, - .cornerRadius = { CORNER_RADIUS, CORNER_RADIUS, 0, 0 }, - .custom = { .customData = tb_grad }, - .border = { .color = g_theme.border, .width = { .bottom = 1 } }, - ) { - // Title text (grows to push close button right) - CLAY(WIDI(id, 3002), - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - } - ) { - CLAY_TEXT(clay_str(title), &g_widget_text_config); - } - - // Close button - Clay_Color close_bg = g_theme_id == 1 - ? (close_hovered ? Clay_Color{220, 50, 50, 255} : Clay_Color{200, 70, 70, 255}) - : (close_hovered ? Clay_Color{200, 60, 60, 255} : Clay_Color{120, 50, 50, 255}); - CLAY(close_id, - .layout = { - .sizing = { .width = CLAY_SIZING_FIXED(uis(20)), .height = CLAY_SIZING_FIXED(uis(20)) }, - .childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER }, - }, - .backgroundColor = close_bg, - .cornerRadius = CLAY_CORNER_RADIUS(CORNER_RADIUS) - ) { - ui_icon(UI_ICON_CLOSE, uis(12), g_theme.button_text); - } - } - - // Content area - CLAY(WIDI(id, 3003), - .layout = { - .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() }, - .padding = { uip(12), uip(12), uip(10), uip(10) }, - .childGap = uip(8), - .layoutDirection = CLAY_TOP_TO_BOTTOM, - } - ) { - if (content_fn) { - content_fn(user_data); - } - } - } - - // Handle close button click - if (close_hovered && g_wstate.mouse_clicked) { - *open = 0; - return 0; - } - - return 1; -} - //////////////////////////////// // Tab bar diff --git a/src/ui/ui_widgets.h b/src/ui/ui_widgets.h index 79c665b..90dca99 100644 --- a/src/ui/ui_widgets.h +++ b/src/ui/ui_widgets.h @@ -15,38 +15,17 @@ #define UI_WIDGET_MAX_DROPDOWN_ITEMS 32 #define UI_WIDGET_MAX_TEXT_INPUTS 16 -#define UI_WIDGET_MAX_WINDOWS 16 - -struct UI_ModalState { - B32 active; - U32 id; // Hash of the modal's string ID - S32 result; // Button index pressed, -1 = pending -}; - -struct UI_WindowSlot { - U32 id; // Hash of the window's string ID (0 = unused) - Vec2F32 position; - Vec2F32 size; - B32 open; - S16 z_order; -}; struct UI_KnobDragState { - U32 dragging_id; // Hash of the knob being dragged (0 = none) + U32 dragging_id; // Hash of the knob being dragged (0 = none) F32 drag_start_y; // Mouse Y when drag started F32 drag_start_x; // Mouse X when drag started (for h-slider) F32 value_at_start; // Value when drag started B32 was_shift; // Shift state last frame (to re-anchor on change) - U32 last_click_id; // Knob hash of last click (for F64-click detection) + U32 last_click_id; // Knob hash of last click (for F64-click detection) S32 last_click_frame; // Frame number of last click }; -struct UI_DragState { - U32 dragging_id; // Window ID currently being dragged (0 = none) - Vec2F32 drag_anchor; // Mouse position when drag started - Vec2F32 pos_anchor; // Window position when drag started -}; - struct UI_WidgetState { // Text input focus U32 focused_id; // Clay element ID hash of the focused text input (0 = none) @@ -58,8 +37,8 @@ struct UI_WidgetState { S32 sel_end; // Selection extent (moves with cursor) // Tab cycling: registered text input IDs in order of declaration - U32 text_input_ids[UI_WIDGET_MAX_TEXT_INPUTS]; - S32 text_input_count; + U32 text_input_ids[UI_WIDGET_MAX_TEXT_INPUTS]; + S32 text_input_count; B32 tab_pressed; // True on the frame Tab was pressed // Dropdown @@ -71,15 +50,6 @@ struct UI_WidgetState { // Click detection B32 mouse_clicked; // true on the frame mouse transitions from up->down - // Modal state - UI_ModalState modal; - - // Window state - UI_WindowSlot windows[UI_WIDGET_MAX_WINDOWS]; - S32 window_count; - S16 next_z; - UI_DragState drag; - // Knob drag state UI_KnobDragState knob_drag; @@ -134,22 +104,12 @@ B32 ui_text_input(const char *id, char *buf, S32 buf_size); // options is an array of label strings, count is the number of options. B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected); -// Modal dialog. Returns button index pressed (0-based), -1 if pending, -2 if Escape dismissed. -// Call every frame while active — it draws the overlay and dialog box. -S32 ui_modal(const char *id, const char *title, const char *message, - const char **buttons, S32 button_count); -B32 ui_modal_is_active(); - // Tab bar. Renders a row of tabs with active/inactive states. // Returns the currently selected index. Clicking an inactive tab updates *selected. S32 ui_tab_bar(const char *id, const char **labels, S32 count, S32 *selected); -// Draggable floating window. content_fn is called inside the window body each frame. -// *open is set to 0 when the close button is clicked. Returns true while window is open. +// Content function type (used by popup windows) typedef void (*UI_WindowContentFn)(void *user_data); -B32 ui_window(const char *id, const char *title, B32 *open, - Vec2F32 initial_pos, Vec2F32 initial_size, - UI_WindowContentFn content_fn, void *user_data); // Knob / potentiometer. Vertical drag to change value. // unsigned (is_signed=0): value in [0, max_val]