diff --git a/src/main.cpp b/src/main.cpp index a3a521d..e30ca21 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -65,6 +65,14 @@ struct AppState { char demo_text_a[128]; char demo_text_b[128]; 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" }; 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 @@ -485,6 +543,26 @@ static void build_ui(AppState *app) { 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; + } + } } //////////////////////////////// diff --git a/src/renderer/renderer_dx12.cpp b/src/renderer/renderer_dx12.cpp index a8d5d65..55a9b73 100644 --- a/src/renderer/renderer_dx12.cpp +++ b/src/renderer/renderer_dx12.cpp @@ -874,9 +874,9 @@ static void emit_text_glyphs(DrawBatch *batch, Renderer *r, F32 scale = (F32)font_size / r->font_atlas_size; F32 text_h = r->font_line_height * scale; - // Vertically center text in bounding box - F32 x = bbox.x; - F32 y = bbox.y + (bbox.height - text_h) * 0.5f; + // Vertically center text in bounding box, snapped to pixel grid to avoid blurry glyphs + F32 x = floorf(bbox.x + 0.5f); + F32 y = floorf(bbox.y + (bbox.height - text_h) * 0.5f + 0.5f); for (int32_t i = 0; i < text_len; i++) { char ch = text[i]; diff --git a/src/ui/ui_widgets.cpp b/src/ui/ui_widgets.cpp index 701de51..462498f 100644 --- a/src/ui/ui_widgets.cpp +++ b/src/ui/ui_widgets.cpp @@ -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; +} diff --git a/src/ui/ui_widgets.h b/src/ui/ui_widgets.h index f8788b5..33ffb5b 100644 --- a/src/ui/ui_widgets.h +++ b/src/ui/ui_widgets.h @@ -14,6 +14,27 @@ #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; + 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 { // Text input focus @@ -38,6 +59,15 @@ 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; + int16_t next_z; + UI_DragState drag; }; 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. // 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(); + +// 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);