Use platform windows for windowing instead of custom draggable window system

This commit is contained in:
2026-03-05 11:44:23 -05:00
parent b75cb920eb
commit 5eaae4deb9
15 changed files with 833 additions and 742 deletions

4
.vscode/launch.json vendored
View File

@@ -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"
}
]
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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 <NSWindowDelegate>
@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 (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 <NSTextInputClient>
@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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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))
{

View File

@@ -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.

View File

@@ -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

136
src/ui/ui_piano.cpp Normal file
View File

@@ -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;
}
}

20
src/ui/ui_piano.h Normal file
View File

@@ -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);

143
src/ui/ui_popups.cpp Normal file
View File

@@ -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]);
}
}

31
src/ui/ui_popups.h Normal file
View File

@@ -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);

View File

@@ -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

View File

@@ -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]