Use platform windows for windowing instead of custom draggable window system
This commit is contained in:
@@ -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
136
src/ui/ui_piano.cpp
Normal 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
20
src/ui/ui_piano.h
Normal 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
143
src/ui/ui_popups.cpp
Normal 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
31
src/ui/ui_popups.h
Normal 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);
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user