Add modals!

This commit is contained in:
2026-03-03 01:13:04 -05:00
parent b469b8212f
commit 7902db6ec7
4 changed files with 442 additions and 3 deletions

View File

@@ -29,6 +29,11 @@ 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();
}
@@ -731,3 +736,316 @@ 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,
}
) {}
// Dialog box (centered)
CLAY(WIDI(id, 2001),
.layout = {
.sizing = { .width = CLAY_SIZING_FIXED(400), .height = CLAY_SIZING_FIT() },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.backgroundColor = g_theme.bg_medium,
.cornerRadius = CLAY_CORNER_RADIUS(6),
.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
CLAY(WIDI(id, 2002),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(32) },
.padding = { 12, 12, 0, 0 },
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
},
.backgroundColor = g_theme.title_bar,
.cornerRadius = { 6, 6, 0, 0 },
.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 = { 16, 16, 16, 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 = { 16, 16, 8, 16 },
.childGap = 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(btn_id,
.layout = {
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(30) },
.padding = { 16, 16, 0, 0 },
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
},
.backgroundColor = btn_hovered ? g_theme.accent_hover : g_theme.accent,
.cornerRadius = CLAY_CORNER_RADIUS(3)
) {
CLAY_TEXT(clay_str(buttons[i]), &g_widget_text_config);
}
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(uint32_t 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) {
int16_t 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]) {
int16_t 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 = (int16_t)i;
}
}
}
g_wstate.next_z = (int16_t)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);
// Window floating element
CLAY(eid,
.layout = {
.sizing = { .width = CLAY_SIZING_FIXED(slot->size.x), .height = CLAY_SIZING_FIT() },
.layoutDirection = CLAY_TOP_TO_BOTTOM,
},
.backgroundColor = g_theme.bg_medium,
.cornerRadius = CLAY_CORNER_RADIUS(6),
.floating = {
.offset = { slot->position.x, slot->position.y },
.zIndex = (int16_t)(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
CLAY(title_bar_id,
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(28) },
.padding = { 10, 10, 0, 0 },
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
.layoutDirection = CLAY_LEFT_TO_RIGHT,
},
.backgroundColor = g_theme.title_bar,
.cornerRadius = { 6, 6, 0, 0 },
.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 = close_hovered ? Clay_Color{200, 60, 60, 255} : Clay_Color{120, 50, 50, 255};
CLAY(close_id,
.layout = {
.sizing = { .width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIXED(20) },
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
},
.backgroundColor = close_bg,
.cornerRadius = CLAY_CORNER_RADIUS(3)
) {
CLAY_TEXT(CLAY_STRING("x"), &g_widget_text_config);
}
}
// Content area
CLAY(WIDI(id, 3003),
.layout = {
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
.padding = { 12, 12, 10, 10 },
.childGap = 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;
}