Add modals!
This commit is contained in:
78
src/main.cpp
78
src/main.cpp
@@ -65,6 +65,14 @@ struct AppState {
|
|||||||
char demo_text_a[128];
|
char demo_text_a[128];
|
||||||
char demo_text_b[128];
|
char demo_text_b[128];
|
||||||
int32_t demo_button_count;
|
int32_t demo_button_count;
|
||||||
|
|
||||||
|
// Modal / window demo state
|
||||||
|
B32 show_settings_window;
|
||||||
|
B32 show_about_window;
|
||||||
|
B32 show_confirm_dialog;
|
||||||
|
S32 settings_theme_sel;
|
||||||
|
B32 settings_vsync;
|
||||||
|
B32 settings_autosave;
|
||||||
};
|
};
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
@@ -221,6 +229,31 @@ static void build_main_panel(AppState *app) {
|
|||||||
static const char *rate_options[] = { "44100 Hz", "48000 Hz", "88200 Hz", "96000 Hz", "192000 Hz" };
|
static const char *rate_options[] = { "44100 Hz", "48000 Hz", "88200 Hz", "96000 Hz", "192000 Hz" };
|
||||||
ui_dropdown("DropRate", rate_options, 5, &app->demo_dropdown_sel);
|
ui_dropdown("DropRate", rate_options, 5, &app->demo_dropdown_sel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CLAY(CLAY_ID("Sep5"),
|
||||||
|
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
|
||||||
|
.backgroundColor = g_theme.border
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// Section: Windows & Modals
|
||||||
|
ui_label("LblWindows", "Windows & Modals");
|
||||||
|
CLAY(CLAY_ID("WindowBtnRow"),
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
|
||||||
|
.childGap = 8,
|
||||||
|
.layoutDirection = CLAY_LEFT_TO_RIGHT,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (ui_button("BtnSettings", "Settings")) {
|
||||||
|
app->show_settings_window = 1;
|
||||||
|
}
|
||||||
|
if (ui_button("BtnAbout", "About")) {
|
||||||
|
app->show_about_window = 1;
|
||||||
|
}
|
||||||
|
if (ui_button("BtnConfirm", "Confirm Dialog")) {
|
||||||
|
app->show_confirm_dialog = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,6 +482,31 @@ static void build_log_panel(B32 show) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Window content callbacks
|
||||||
|
|
||||||
|
static void settings_window_content(void *user_data) {
|
||||||
|
AppState *app = (AppState *)user_data;
|
||||||
|
|
||||||
|
ui_label("SettingsLblTheme", "Theme");
|
||||||
|
static const char *theme_options[] = { "Dark", "Light", "Solarized" };
|
||||||
|
CLAY(CLAY_ID("SettingsThemeWrap"),
|
||||||
|
.layout = { .sizing = { .width = CLAY_SIZING_FIXED(180), .height = CLAY_SIZING_FIT() } }
|
||||||
|
) {
|
||||||
|
ui_dropdown("SettingsTheme", theme_options, 3, &app->settings_theme_sel);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_checkbox("SettingsVsync", "V-Sync", &app->settings_vsync);
|
||||||
|
ui_checkbox("SettingsAutosave", "Autosave", &app->settings_autosave);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void about_window_content(void *user_data) {
|
||||||
|
(void)user_data;
|
||||||
|
ui_label("AboutLbl1", "autosample v0.1");
|
||||||
|
ui_label("AboutLbl2", "An audio sampling workstation.");
|
||||||
|
ui_label("AboutLbl3", "Built with Clay UI.");
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
// Build the full UI layout for one frame
|
// Build the full UI layout for one frame
|
||||||
|
|
||||||
@@ -485,6 +543,26 @@ static void build_ui(AppState *app) {
|
|||||||
|
|
||||||
build_log_panel(app->show_log);
|
build_log_panel(app->show_log);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draggable windows (rendered as floating elements above normal UI)
|
||||||
|
ui_window("WinSettings", "Settings", &app->show_settings_window,
|
||||||
|
Vec2F32{100, 100}, Vec2F32{280, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
|
|||||||
@@ -874,9 +874,9 @@ static void emit_text_glyphs(DrawBatch *batch, Renderer *r,
|
|||||||
F32 scale = (F32)font_size / r->font_atlas_size;
|
F32 scale = (F32)font_size / r->font_atlas_size;
|
||||||
F32 text_h = r->font_line_height * scale;
|
F32 text_h = r->font_line_height * scale;
|
||||||
|
|
||||||
// Vertically center text in bounding box
|
// Vertically center text in bounding box, snapped to pixel grid to avoid blurry glyphs
|
||||||
F32 x = bbox.x;
|
F32 x = floorf(bbox.x + 0.5f);
|
||||||
F32 y = bbox.y + (bbox.height - text_h) * 0.5f;
|
F32 y = floorf(bbox.y + (bbox.height - text_h) * 0.5f + 0.5f);
|
||||||
|
|
||||||
for (int32_t i = 0; i < text_len; i++) {
|
for (int32_t i = 0; i < text_len; i++) {
|
||||||
char ch = text[i];
|
char ch = text[i];
|
||||||
|
|||||||
@@ -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();
|
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;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,27 @@
|
|||||||
|
|
||||||
#define UI_WIDGET_MAX_DROPDOWN_ITEMS 32
|
#define UI_WIDGET_MAX_DROPDOWN_ITEMS 32
|
||||||
#define UI_WIDGET_MAX_TEXT_INPUTS 16
|
#define UI_WIDGET_MAX_TEXT_INPUTS 16
|
||||||
|
#define UI_WIDGET_MAX_WINDOWS 16
|
||||||
|
|
||||||
|
struct UI_ModalState {
|
||||||
|
B32 active;
|
||||||
|
uint32_t id; // Hash of the modal's string ID
|
||||||
|
S32 result; // Button index pressed, -1 = pending
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UI_WindowSlot {
|
||||||
|
uint32_t id; // Hash of the window's string ID (0 = unused)
|
||||||
|
Vec2F32 position;
|
||||||
|
Vec2F32 size;
|
||||||
|
B32 open;
|
||||||
|
int16_t z_order;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UI_DragState {
|
||||||
|
uint32_t 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 {
|
struct UI_WidgetState {
|
||||||
// Text input focus
|
// Text input focus
|
||||||
@@ -38,6 +59,15 @@ struct UI_WidgetState {
|
|||||||
|
|
||||||
// Click detection
|
// Click detection
|
||||||
B32 mouse_clicked; // true on the frame mouse transitions from up->down
|
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;
|
||||||
|
int16_t next_z;
|
||||||
|
UI_DragState drag;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern UI_WidgetState g_wstate;
|
extern UI_WidgetState g_wstate;
|
||||||
@@ -76,3 +106,16 @@ B32 ui_text_input(const char *id, char *buf, S32 buf_size);
|
|||||||
// Dropdown / combo box. Sets *selected to chosen index. Returns true if changed.
|
// Dropdown / combo box. Sets *selected to chosen index. Returns true if changed.
|
||||||
// options is an array of label strings, count is the number of options.
|
// 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);
|
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();
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user