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

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]