fix ui widgets
This commit is contained in:
@@ -17,7 +17,7 @@ target:
|
|||||||
debug_info:
|
debug_info:
|
||||||
{
|
{
|
||||||
path: "C:/Users/mta/projects/autosample/build/autosample.pdb"
|
path: "C:/Users/mta/projects/autosample/build/autosample.pdb"
|
||||||
timestamp: 66207540958809
|
timestamp: 66207544041290
|
||||||
}
|
}
|
||||||
target:
|
target:
|
||||||
{
|
{
|
||||||
|
|||||||
151
src/main.cpp
151
src/main.cpp
@@ -75,6 +75,32 @@ static void init_text_configs() {
|
|||||||
g_text_config_dim.wrapMode = CLAY_TEXT_WRAP_NONE;
|
g_text_config_dim.wrapMode = CLAY_TEXT_WRAP_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// App state — all mutable state the frame function needs
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
PlatformWindow *window;
|
||||||
|
Renderer *renderer;
|
||||||
|
MidiEngine *midi;
|
||||||
|
UI_Context *ui;
|
||||||
|
S32 last_w, last_h;
|
||||||
|
B32 show_browser;
|
||||||
|
B32 show_props;
|
||||||
|
B32 show_log;
|
||||||
|
B32 show_midi_devices;
|
||||||
|
LARGE_INTEGER freq;
|
||||||
|
LARGE_INTEGER last_time;
|
||||||
|
|
||||||
|
// Demo widget state
|
||||||
|
B32 demo_checkbox_a;
|
||||||
|
B32 demo_checkbox_b;
|
||||||
|
int32_t demo_radio_sel;
|
||||||
|
int32_t demo_dropdown_sel;
|
||||||
|
char demo_text_a[128];
|
||||||
|
char demo_text_b[128];
|
||||||
|
int32_t demo_button_count;
|
||||||
|
};
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
// Panel builders
|
// Panel builders
|
||||||
|
|
||||||
@@ -118,7 +144,7 @@ static void build_browser_panel(B32 show) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void build_main_panel() {
|
static void build_main_panel(AppState *app) {
|
||||||
CLAY(CLAY_ID("MainPanel"),
|
CLAY(CLAY_ID("MainPanel"),
|
||||||
.layout = {
|
.layout = {
|
||||||
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
|
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
|
||||||
@@ -131,12 +157,104 @@ static void build_main_panel() {
|
|||||||
CLAY(CLAY_ID("MainContent"),
|
CLAY(CLAY_ID("MainContent"),
|
||||||
.layout = {
|
.layout = {
|
||||||
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
|
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_GROW() },
|
||||||
.padding = { 8, 8, 6, 6 },
|
.padding = { 16, 16, 12, 12 },
|
||||||
.childGap = 4,
|
.childGap = 12,
|
||||||
.layoutDirection = CLAY_TOP_TO_BOTTOM,
|
.layoutDirection = CLAY_TOP_TO_BOTTOM,
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
CLAY_TEXT(CLAY_STRING("Main content area"), &g_text_config_normal);
|
// Section: Buttons
|
||||||
|
ui_label("LblButtons", "Buttons");
|
||||||
|
CLAY(CLAY_ID("ButtonRow"),
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
|
||||||
|
.childGap = 8,
|
||||||
|
.layoutDirection = CLAY_LEFT_TO_RIGHT,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (ui_button("BtnHello", "Click Me")) {
|
||||||
|
app->demo_button_count++;
|
||||||
|
}
|
||||||
|
if (ui_button("BtnReset", "Reset")) {
|
||||||
|
app->demo_button_count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Show click count
|
||||||
|
static char btn_count_buf[64];
|
||||||
|
snprintf(btn_count_buf, sizeof(btn_count_buf), "Button clicked %d times", app->demo_button_count);
|
||||||
|
ui_label("LblBtnCount", btn_count_buf);
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
CLAY(CLAY_ID("Sep1"),
|
||||||
|
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
|
||||||
|
.backgroundColor = g_theme.border
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// Section: Checkboxes
|
||||||
|
ui_label("LblCheckboxes", "Checkboxes");
|
||||||
|
ui_checkbox("ChkA", "Enable feature A", &app->demo_checkbox_a);
|
||||||
|
ui_checkbox("ChkB", "Enable feature B", &app->demo_checkbox_b);
|
||||||
|
|
||||||
|
CLAY(CLAY_ID("Sep2"),
|
||||||
|
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
|
||||||
|
.backgroundColor = g_theme.border
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// Section: Radio buttons
|
||||||
|
ui_label("LblRadio", "Output Format");
|
||||||
|
static const char *radio_options[] = { "WAV", "AIFF", "FLAC" };
|
||||||
|
ui_radio_group("RadioFmt", radio_options, 3, &app->demo_radio_sel);
|
||||||
|
|
||||||
|
CLAY(CLAY_ID("Sep3"),
|
||||||
|
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
|
||||||
|
.backgroundColor = g_theme.border
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// Section: Text inputs
|
||||||
|
ui_label("LblText", "Text Inputs");
|
||||||
|
CLAY(CLAY_ID("TextRow"),
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
|
||||||
|
.childGap = 8,
|
||||||
|
.layoutDirection = CLAY_LEFT_TO_RIGHT,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
CLAY(CLAY_ID("TextCol1"),
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
|
||||||
|
.childGap = 4,
|
||||||
|
.layoutDirection = CLAY_TOP_TO_BOTTOM,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
ui_label("LblName", "Name:");
|
||||||
|
ui_text_input("TxtName", app->demo_text_a, sizeof(app->demo_text_a));
|
||||||
|
}
|
||||||
|
CLAY(CLAY_ID("TextCol2"),
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
|
||||||
|
.childGap = 4,
|
||||||
|
.layoutDirection = CLAY_TOP_TO_BOTTOM,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
ui_label("LblPath", "Output Path:");
|
||||||
|
ui_text_input("TxtPath", app->demo_text_b, sizeof(app->demo_text_b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAY(CLAY_ID("Sep4"),
|
||||||
|
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) } },
|
||||||
|
.backgroundColor = g_theme.border
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// Section: Dropdown
|
||||||
|
ui_label("LblDropdown", "Sample Rate");
|
||||||
|
CLAY(CLAY_ID("DropdownWrapper"),
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_FIXED(200), .height = CLAY_SIZING_FIT() },
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,23 +367,6 @@ static void build_log_panel(B32 show) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////
|
|
||||||
// App state — all mutable state the frame function needs
|
|
||||||
|
|
||||||
struct AppState {
|
|
||||||
PlatformWindow *window;
|
|
||||||
Renderer *renderer;
|
|
||||||
MidiEngine *midi;
|
|
||||||
UI_Context *ui;
|
|
||||||
S32 last_w, last_h;
|
|
||||||
B32 show_browser;
|
|
||||||
B32 show_props;
|
|
||||||
B32 show_log;
|
|
||||||
B32 show_midi_devices;
|
|
||||||
LARGE_INTEGER freq;
|
|
||||||
LARGE_INTEGER last_time;
|
|
||||||
};
|
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
// Build the full UI layout for one frame
|
// Build the full UI layout for one frame
|
||||||
|
|
||||||
@@ -284,7 +385,7 @@ static void build_ui(AppState *app) {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
build_browser_panel(app->show_browser);
|
build_browser_panel(app->show_browser);
|
||||||
build_main_panel();
|
build_main_panel(app);
|
||||||
|
|
||||||
if (app->show_props || app->show_midi_devices) {
|
if (app->show_props || app->show_midi_devices) {
|
||||||
CLAY(CLAY_ID("RightColumn"),
|
CLAY(CLAY_ID("RightColumn"),
|
||||||
@@ -330,6 +431,8 @@ static void do_frame(AppState *app) {
|
|||||||
|
|
||||||
// Gather input
|
// Gather input
|
||||||
input_gather(platform_get_native_handle(app->window));
|
input_gather(platform_get_native_handle(app->window));
|
||||||
|
PlatformInputEvents input_events = platform_get_input_events(app->window);
|
||||||
|
ui_widgets_begin_frame(input_events, g_input.mouse_down, g_input.was_mouse_down);
|
||||||
|
|
||||||
// Build UI with Clay
|
// Build UI with Clay
|
||||||
ui_begin_frame(app->ui, (F32)w, (F32)h, g_input.mouse, g_input.mouse_down,
|
ui_begin_frame(app->ui, (F32)w, (F32)h, g_input.mouse, g_input.mouse_down,
|
||||||
@@ -382,6 +485,7 @@ int main(int argc, char **argv) {
|
|||||||
init_text_configs();
|
init_text_configs();
|
||||||
|
|
||||||
setup_menus(window);
|
setup_menus(window);
|
||||||
|
ui_widgets_init();
|
||||||
|
|
||||||
AppState app = {};
|
AppState app = {};
|
||||||
app.window = window;
|
app.window = window;
|
||||||
@@ -394,6 +498,9 @@ int main(int argc, char **argv) {
|
|||||||
app.show_props = 1;
|
app.show_props = 1;
|
||||||
app.show_log = 1;
|
app.show_log = 1;
|
||||||
app.show_midi_devices = 1;
|
app.show_midi_devices = 1;
|
||||||
|
app.demo_dropdown_sel = 1; // default to 48000 Hz
|
||||||
|
snprintf(app.demo_text_a, sizeof(app.demo_text_a), "My Instrument");
|
||||||
|
snprintf(app.demo_text_b, sizeof(app.demo_text_b), "C:\\Samples\\output");
|
||||||
QueryPerformanceFrequency(&app.freq);
|
QueryPerformanceFrequency(&app.freq);
|
||||||
QueryPerformanceCounter(&app.last_time);
|
QueryPerformanceCounter(&app.last_time);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,46 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Input event buffer
|
||||||
|
// Accumulated per frame, consumed by the app each tick.
|
||||||
|
|
||||||
|
#define PLATFORM_MAX_CHARS_PER_FRAME 64
|
||||||
|
#define PLATFORM_MAX_KEYS_PER_FRAME 32
|
||||||
|
|
||||||
|
// Virtual key codes (subset matching Win32 VK_ codes)
|
||||||
|
enum {
|
||||||
|
PKEY_BACKSPACE = 0x08,
|
||||||
|
PKEY_TAB = 0x09,
|
||||||
|
PKEY_RETURN = 0x0D,
|
||||||
|
PKEY_ESCAPE = 0x1B,
|
||||||
|
PKEY_DELETE = 0x2E,
|
||||||
|
PKEY_LEFT = 0x25,
|
||||||
|
PKEY_UP = 0x26,
|
||||||
|
PKEY_RIGHT = 0x27,
|
||||||
|
PKEY_DOWN = 0x28,
|
||||||
|
PKEY_HOME = 0x24,
|
||||||
|
PKEY_END = 0x23,
|
||||||
|
PKEY_A = 0x41,
|
||||||
|
PKEY_C = 0x43,
|
||||||
|
PKEY_V = 0x56,
|
||||||
|
PKEY_X = 0x58,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PlatformInputEvents {
|
||||||
|
// Typed characters (UTF-16 code units, printable only)
|
||||||
|
uint16_t chars[PLATFORM_MAX_CHARS_PER_FRAME];
|
||||||
|
int32_t char_count;
|
||||||
|
|
||||||
|
// Key-down events (virtual key codes)
|
||||||
|
uint8_t keys[PLATFORM_MAX_KEYS_PER_FRAME];
|
||||||
|
int32_t key_count;
|
||||||
|
|
||||||
|
// Modifier state at time of last key event
|
||||||
|
bool ctrl_held;
|
||||||
|
bool shift_held;
|
||||||
|
};
|
||||||
|
|
||||||
struct PlatformWindow;
|
struct PlatformWindow;
|
||||||
|
|
||||||
struct PlatformWindowDesc {
|
struct PlatformWindowDesc {
|
||||||
@@ -36,3 +76,6 @@ void *platform_get_native_handle(PlatformWindow *window);
|
|||||||
void platform_set_frame_callback(PlatformWindow *window, PlatformFrameCallback cb, void *user_data);
|
void platform_set_frame_callback(PlatformWindow *window, PlatformFrameCallback cb, void *user_data);
|
||||||
void platform_set_menu(PlatformWindow *window, PlatformMenu *menus, int32_t menu_count);
|
void platform_set_menu(PlatformWindow *window, PlatformMenu *menus, int32_t menu_count);
|
||||||
int32_t platform_poll_menu_command(PlatformWindow *window);
|
int32_t platform_poll_menu_command(PlatformWindow *window);
|
||||||
|
|
||||||
|
// Returns accumulated input events since last call, then clears the buffer.
|
||||||
|
PlatformInputEvents platform_get_input_events(PlatformWindow *window);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ struct PlatformWindow {
|
|||||||
int32_t pending_menu_cmd;
|
int32_t pending_menu_cmd;
|
||||||
PlatformFrameCallback frame_callback;
|
PlatformFrameCallback frame_callback;
|
||||||
void *frame_callback_user_data;
|
void *frame_callback_user_data;
|
||||||
|
PlatformInputEvents input_events;
|
||||||
};
|
};
|
||||||
|
|
||||||
static PlatformWindow *g_current_window = nullptr;
|
static PlatformWindow *g_current_window = nullptr;
|
||||||
@@ -31,10 +32,36 @@ static LRESULT CALLBACK win32_wndproc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
case WM_CHAR:
|
||||||
|
if (g_current_window && wparam >= 32 && wparam < 0xFFFF) {
|
||||||
|
PlatformInputEvents *ev = &g_current_window->input_events;
|
||||||
|
if (ev->char_count < PLATFORM_MAX_CHARS_PER_FRAME)
|
||||||
|
ev->chars[ev->char_count++] = (uint16_t)wparam;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
case WM_KEYDOWN:
|
||||||
|
case WM_SYSKEYDOWN:
|
||||||
|
if (g_current_window) {
|
||||||
|
PlatformInputEvents *ev = &g_current_window->input_events;
|
||||||
|
if (ev->key_count < PLATFORM_MAX_KEYS_PER_FRAME)
|
||||||
|
ev->keys[ev->key_count++] = (uint8_t)wparam;
|
||||||
|
ev->ctrl_held = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||||
|
ev->shift_held = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
|
||||||
|
}
|
||||||
|
break; // fall through to DefWindowProc for system keys
|
||||||
case WM_COMMAND:
|
case WM_COMMAND:
|
||||||
if (g_current_window && HIWORD(wparam) == 0)
|
if (g_current_window && HIWORD(wparam) == 0)
|
||||||
g_current_window->pending_menu_cmd = (int32_t)LOWORD(wparam);
|
g_current_window->pending_menu_cmd = (int32_t)LOWORD(wparam);
|
||||||
return 0;
|
return 0;
|
||||||
|
case WM_SETCURSOR:
|
||||||
|
// When the cursor is in our client area, force it to an arrow.
|
||||||
|
// Without this, moving from a resize border back into the client
|
||||||
|
// area would leave the resize cursor shape stuck.
|
||||||
|
if (LOWORD(lparam) == HTCLIENT) {
|
||||||
|
SetCursor(LoadCursor(nullptr, IDC_ARROW));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case WM_CLOSE:
|
case WM_CLOSE:
|
||||||
if (g_current_window)
|
if (g_current_window)
|
||||||
g_current_window->should_close = true;
|
g_current_window->should_close = true;
|
||||||
@@ -56,6 +83,7 @@ PlatformWindow *platform_create_window(PlatformWindowDesc *desc) {
|
|||||||
wc.style = CS_CLASSDC;
|
wc.style = CS_CLASSDC;
|
||||||
wc.lpfnWndProc = win32_wndproc;
|
wc.lpfnWndProc = win32_wndproc;
|
||||||
wc.hInstance = GetModuleHandleW(nullptr);
|
wc.hInstance = GetModuleHandleW(nullptr);
|
||||||
|
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||||
wc.lpszClassName = L"autosample_wc";
|
wc.lpszClassName = L"autosample_wc";
|
||||||
RegisterClassExW(&wc);
|
RegisterClassExW(&wc);
|
||||||
|
|
||||||
@@ -172,3 +200,9 @@ int32_t platform_poll_menu_command(PlatformWindow *window) {
|
|||||||
window->pending_menu_cmd = 0;
|
window->pending_menu_cmd = 0;
|
||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PlatformInputEvents platform_get_input_events(PlatformWindow *window) {
|
||||||
|
PlatformInputEvents result = window->input_events;
|
||||||
|
window->input_events = {};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,483 @@
|
|||||||
// ui_widgets.cpp - Removed: Clay handles all layout and widgets directly.
|
// ui_widgets.cpp - Immediate-mode widget implementations on top of Clay.
|
||||||
// This file is kept empty for the unity build include order.
|
//
|
||||||
|
// IMPORTANT: Clay_Hovered() only works inside CLAY() macro declaration args
|
||||||
|
// (where the element is the "open" element on the stack). For hover checks
|
||||||
|
// AFTER a CLAY() block, use Clay_PointerOver(elementId).
|
||||||
|
|
||||||
|
#include "ui/ui_widgets.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
UI_WidgetState g_wstate = {};
|
||||||
|
|
||||||
|
void ui_widgets_init() {
|
||||||
|
g_wstate = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_widgets_begin_frame(PlatformInputEvents input, B32 mouse_down, B32 was_mouse_down) {
|
||||||
|
g_wstate.input = input;
|
||||||
|
g_wstate.was_mouse_down = g_wstate.mouse_down;
|
||||||
|
g_wstate.mouse_down = mouse_down;
|
||||||
|
g_wstate.mouse_clicked = (mouse_down && !g_wstate.was_mouse_down);
|
||||||
|
g_wstate.cursor_blink += 1.0f / 60.0f;
|
||||||
|
ui_text_input_reset_display_bufs();
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
static Clay_TextElementConfig g_widget_text_config;
|
||||||
|
static Clay_TextElementConfig g_widget_text_config_dim;
|
||||||
|
static bool g_widget_text_configs_init = false;
|
||||||
|
|
||||||
|
static void ensure_widget_text_configs() {
|
||||||
|
if (g_widget_text_configs_init) return;
|
||||||
|
g_widget_text_configs_init = true;
|
||||||
|
|
||||||
|
g_widget_text_config = {};
|
||||||
|
g_widget_text_config.textColor = g_theme.text;
|
||||||
|
g_widget_text_config.fontSize = 15;
|
||||||
|
g_widget_text_config.wrapMode = CLAY_TEXT_WRAP_NONE;
|
||||||
|
|
||||||
|
g_widget_text_config_dim = {};
|
||||||
|
g_widget_text_config_dim.textColor = g_theme.text_dim;
|
||||||
|
g_widget_text_config_dim.fontSize = 15;
|
||||||
|
g_widget_text_config_dim.wrapMode = CLAY_TEXT_WRAP_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Clay_String clay_str(const char *s) {
|
||||||
|
Clay_String r = {};
|
||||||
|
r.isStaticallyAllocated = false;
|
||||||
|
r.length = (S32)strlen(s);
|
||||||
|
r.chars = s;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build Clay_ElementId from runtime string (CLAY_ID requires string literals)
|
||||||
|
#define WID(s) CLAY_SID(clay_str(s))
|
||||||
|
#define WIDI(s, i) CLAY_SIDI(clay_str(s), i)
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Label
|
||||||
|
|
||||||
|
void ui_label(const char *id, const char *text) {
|
||||||
|
ensure_widget_text_configs();
|
||||||
|
CLAY(WID(id),
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
|
||||||
|
.padding = { 0, 0, 2, 2 },
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
CLAY_TEXT(clay_str(text), &g_widget_text_config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Button
|
||||||
|
|
||||||
|
B32 ui_button(const char *id, const char *text) {
|
||||||
|
ensure_widget_text_configs();
|
||||||
|
|
||||||
|
Clay_ElementId eid = WID(id);
|
||||||
|
B32 hovered = Clay_PointerOver(eid);
|
||||||
|
|
||||||
|
CLAY(eid,
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(30) },
|
||||||
|
.padding = { 12, 12, 0, 0 },
|
||||||
|
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
|
||||||
|
},
|
||||||
|
.backgroundColor = hovered ? g_theme.accent_hover : g_theme.accent,
|
||||||
|
.cornerRadius = CLAY_CORNER_RADIUS(3)
|
||||||
|
) {
|
||||||
|
CLAY_TEXT(clay_str(text), &g_widget_text_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (hovered && g_wstate.mouse_clicked) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Checkbox
|
||||||
|
|
||||||
|
B32 ui_checkbox(const char *id, const char *label, B32 *value) {
|
||||||
|
ensure_widget_text_configs();
|
||||||
|
B32 changed = 0;
|
||||||
|
|
||||||
|
Clay_ElementId eid = WID(id);
|
||||||
|
B32 hovered = Clay_PointerOver(eid);
|
||||||
|
|
||||||
|
CLAY(eid,
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(28) },
|
||||||
|
.childGap = 8,
|
||||||
|
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
|
||||||
|
.layoutDirection = CLAY_LEFT_TO_RIGHT,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// Box
|
||||||
|
Clay_Color box_bg = *value ? g_theme.accent : g_theme.bg_dark;
|
||||||
|
if (hovered) {
|
||||||
|
box_bg = *value ? g_theme.accent_hover : g_theme.bg_lighter;
|
||||||
|
}
|
||||||
|
CLAY(WIDI(id, 1),
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_FIXED(18), .height = CLAY_SIZING_FIXED(18) },
|
||||||
|
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
|
||||||
|
},
|
||||||
|
.backgroundColor = box_bg,
|
||||||
|
.cornerRadius = CLAY_CORNER_RADIUS(3),
|
||||||
|
.border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } }
|
||||||
|
) {
|
||||||
|
if (*value) {
|
||||||
|
CLAY_TEXT(CLAY_STRING("x"), &g_widget_text_config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label
|
||||||
|
CLAY_TEXT(clay_str(label), &g_widget_text_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hovered && g_wstate.mouse_clicked) {
|
||||||
|
*value = !(*value);
|
||||||
|
changed = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Radio group
|
||||||
|
|
||||||
|
B32 ui_radio_group(const char *id, const char **options, S32 count, S32 *selected) {
|
||||||
|
ensure_widget_text_configs();
|
||||||
|
B32 changed = 0;
|
||||||
|
|
||||||
|
CLAY(WID(id),
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
|
||||||
|
.childGap = 4,
|
||||||
|
.layoutDirection = CLAY_TOP_TO_BOTTOM,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
for (S32 i = 0; i < count; i++) {
|
||||||
|
B32 is_selected = (*selected == i);
|
||||||
|
Clay_ElementId row_id = WIDI(id, i + 100);
|
||||||
|
B32 row_hovered = Clay_PointerOver(row_id);
|
||||||
|
|
||||||
|
CLAY(row_id,
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(26) },
|
||||||
|
.childGap = 8,
|
||||||
|
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
|
||||||
|
.layoutDirection = CLAY_LEFT_TO_RIGHT,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// Radio circle
|
||||||
|
Clay_Color dot_bg = is_selected ? g_theme.accent : g_theme.bg_dark;
|
||||||
|
if (row_hovered) {
|
||||||
|
dot_bg = is_selected ? g_theme.accent_hover : g_theme.bg_lighter;
|
||||||
|
}
|
||||||
|
CLAY(WIDI(id, i + 200),
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_FIXED(16), .height = CLAY_SIZING_FIXED(16) },
|
||||||
|
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
|
||||||
|
},
|
||||||
|
.backgroundColor = dot_bg,
|
||||||
|
.cornerRadius = CLAY_CORNER_RADIUS(8),
|
||||||
|
.border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } }
|
||||||
|
) {
|
||||||
|
if (is_selected) {
|
||||||
|
CLAY(WIDI(id, i + 300),
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_FIXED(8), .height = CLAY_SIZING_FIXED(8) },
|
||||||
|
},
|
||||||
|
.backgroundColor = g_theme.text,
|
||||||
|
.cornerRadius = CLAY_CORNER_RADIUS(4)
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label
|
||||||
|
CLAY_TEXT(clay_str(options[i]), &g_widget_text_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row_hovered && g_wstate.mouse_clicked && !is_selected) {
|
||||||
|
*selected = i;
|
||||||
|
changed = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Text input
|
||||||
|
//
|
||||||
|
// Each text input gets its own display buffer so Clay string pointers
|
||||||
|
// remain valid until the end of the frame. We support up to
|
||||||
|
// MAX_TEXT_INPUTS simultaneous text inputs per frame.
|
||||||
|
|
||||||
|
#define MAX_TEXT_INPUTS 8
|
||||||
|
static char g_text_display_bufs[MAX_TEXT_INPUTS][512];
|
||||||
|
static S32 g_text_display_buf_idx = 0;
|
||||||
|
|
||||||
|
void ui_text_input_reset_display_bufs() {
|
||||||
|
g_text_display_buf_idx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
B32 ui_text_input(const char *id, char *buf, S32 buf_size) {
|
||||||
|
ensure_widget_text_configs();
|
||||||
|
B32 text_changed = 0;
|
||||||
|
|
||||||
|
// Grab a unique display buffer for this text input
|
||||||
|
S32 my_buf_idx = g_text_display_buf_idx++;
|
||||||
|
if (my_buf_idx >= MAX_TEXT_INPUTS) my_buf_idx = MAX_TEXT_INPUTS - 1;
|
||||||
|
char *display_buf = g_text_display_bufs[my_buf_idx];
|
||||||
|
|
||||||
|
Clay_ElementId eid = WID(id);
|
||||||
|
B32 hovered = Clay_PointerOver(eid);
|
||||||
|
B32 is_focused = (g_wstate.focused_id == eid.id);
|
||||||
|
|
||||||
|
// Click to focus / unfocus
|
||||||
|
if (g_wstate.mouse_clicked) {
|
||||||
|
if (hovered) {
|
||||||
|
if (!is_focused) {
|
||||||
|
g_wstate.focused_id = eid.id;
|
||||||
|
g_wstate.cursor_pos = (S32)strlen(buf);
|
||||||
|
g_wstate.cursor_blink = 0;
|
||||||
|
is_focused = 1;
|
||||||
|
}
|
||||||
|
} else if (is_focused) {
|
||||||
|
g_wstate.focused_id = 0;
|
||||||
|
is_focused = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process keyboard input if focused
|
||||||
|
if (is_focused) {
|
||||||
|
S32 len = (S32)strlen(buf);
|
||||||
|
|
||||||
|
// Clamp cursor
|
||||||
|
if (g_wstate.cursor_pos > len) g_wstate.cursor_pos = len;
|
||||||
|
if (g_wstate.cursor_pos < 0) g_wstate.cursor_pos = 0;
|
||||||
|
|
||||||
|
// Key events first (backspace generates both WM_KEYDOWN and WM_CHAR;
|
||||||
|
// we handle backspace/delete/arrows via keys, printable chars via chars)
|
||||||
|
for (S32 k = 0; k < g_wstate.input.key_count; k++) {
|
||||||
|
uint8_t key = g_wstate.input.keys[k];
|
||||||
|
switch (key) {
|
||||||
|
case PKEY_BACKSPACE:
|
||||||
|
if (g_wstate.cursor_pos > 0) {
|
||||||
|
memmove(&buf[g_wstate.cursor_pos - 1], &buf[g_wstate.cursor_pos], len - g_wstate.cursor_pos + 1);
|
||||||
|
g_wstate.cursor_pos--;
|
||||||
|
len--;
|
||||||
|
text_changed = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PKEY_DELETE:
|
||||||
|
if (g_wstate.cursor_pos < len) {
|
||||||
|
memmove(&buf[g_wstate.cursor_pos], &buf[g_wstate.cursor_pos + 1], len - g_wstate.cursor_pos);
|
||||||
|
len--;
|
||||||
|
text_changed = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PKEY_LEFT:
|
||||||
|
if (g_wstate.cursor_pos > 0) g_wstate.cursor_pos--;
|
||||||
|
g_wstate.cursor_blink = 0;
|
||||||
|
break;
|
||||||
|
case PKEY_RIGHT:
|
||||||
|
if (g_wstate.cursor_pos < len) g_wstate.cursor_pos++;
|
||||||
|
g_wstate.cursor_blink = 0;
|
||||||
|
break;
|
||||||
|
case PKEY_HOME:
|
||||||
|
g_wstate.cursor_pos = 0;
|
||||||
|
g_wstate.cursor_blink = 0;
|
||||||
|
break;
|
||||||
|
case PKEY_END:
|
||||||
|
g_wstate.cursor_pos = len;
|
||||||
|
g_wstate.cursor_blink = 0;
|
||||||
|
break;
|
||||||
|
case PKEY_RETURN:
|
||||||
|
case PKEY_ESCAPE:
|
||||||
|
g_wstate.focused_id = 0;
|
||||||
|
is_focused = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Character input (printable only, skip control chars)
|
||||||
|
if (is_focused) {
|
||||||
|
for (S32 c = 0; c < g_wstate.input.char_count; c++) {
|
||||||
|
uint16_t ch = g_wstate.input.chars[c];
|
||||||
|
if (ch >= 32 && ch < 127 && len < buf_size - 1) {
|
||||||
|
memmove(&buf[g_wstate.cursor_pos + 1], &buf[g_wstate.cursor_pos], len - g_wstate.cursor_pos + 1);
|
||||||
|
buf[g_wstate.cursor_pos] = (char)ch;
|
||||||
|
g_wstate.cursor_pos++;
|
||||||
|
len++;
|
||||||
|
text_changed = 1;
|
||||||
|
g_wstate.cursor_blink = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the display string.
|
||||||
|
// The cursor is shown as a '|' character when focused. To avoid layout
|
||||||
|
// flicker from the cursor blinking (which changes text width), we always
|
||||||
|
// include the cursor character when focused and simply don't blink it.
|
||||||
|
S32 len = (S32)strlen(buf);
|
||||||
|
|
||||||
|
if (is_focused) {
|
||||||
|
S32 cp = g_wstate.cursor_pos;
|
||||||
|
if (cp > len) cp = len;
|
||||||
|
S32 total = len + 1; // +1 for cursor char
|
||||||
|
if (total > 510) total = 510;
|
||||||
|
S32 before = cp < total ? cp : total;
|
||||||
|
memcpy(display_buf, buf, before);
|
||||||
|
display_buf[before] = '|';
|
||||||
|
S32 after = total - before - 1;
|
||||||
|
if (after > 0) memcpy(display_buf + before + 1, buf + cp, after);
|
||||||
|
display_buf[total] = '\0';
|
||||||
|
} else {
|
||||||
|
S32 copy_len = len < 511 ? len : 511;
|
||||||
|
memcpy(display_buf, buf, copy_len);
|
||||||
|
display_buf[copy_len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *show_text = display_buf;
|
||||||
|
Clay_TextElementConfig *text_cfg = &g_widget_text_config;
|
||||||
|
if (len == 0 && !is_focused) {
|
||||||
|
show_text = "...";
|
||||||
|
text_cfg = &g_widget_text_config_dim;
|
||||||
|
}
|
||||||
|
|
||||||
|
Clay_Color bg = is_focused ? g_theme.bg_dark : g_theme.bg_medium;
|
||||||
|
Clay_Color border_color = is_focused ? g_theme.accent : g_theme.border;
|
||||||
|
|
||||||
|
CLAY(eid,
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(30) },
|
||||||
|
.padding = { 8, 8, 0, 0 },
|
||||||
|
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
|
||||||
|
},
|
||||||
|
.backgroundColor = bg,
|
||||||
|
.cornerRadius = CLAY_CORNER_RADIUS(3),
|
||||||
|
.border = { .color = border_color, .width = { 1, 1, 1, 1 } }
|
||||||
|
) {
|
||||||
|
CLAY_TEXT(clay_str(show_text), text_cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return text_changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Dropdown
|
||||||
|
|
||||||
|
B32 ui_dropdown(const char *id, const char **options, S32 count, S32 *selected) {
|
||||||
|
ensure_widget_text_configs();
|
||||||
|
B32 changed = 0;
|
||||||
|
|
||||||
|
Clay_ElementId eid = WID(id);
|
||||||
|
B32 header_hovered = Clay_PointerOver(eid);
|
||||||
|
B32 is_open = (g_wstate.open_dropdown_id == eid.id);
|
||||||
|
|
||||||
|
// Display current selection
|
||||||
|
const char *current_label = (*selected >= 0 && *selected < count) ? options[*selected] : "Select...";
|
||||||
|
|
||||||
|
Clay_Color bg = is_open ? g_theme.bg_dark : g_theme.bg_medium;
|
||||||
|
if (header_hovered && !is_open) bg = g_theme.bg_lighter;
|
||||||
|
|
||||||
|
CLAY(eid,
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(30) },
|
||||||
|
.padding = { 8, 8, 0, 0 },
|
||||||
|
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
|
||||||
|
.layoutDirection = CLAY_LEFT_TO_RIGHT,
|
||||||
|
},
|
||||||
|
.backgroundColor = bg,
|
||||||
|
.cornerRadius = CLAY_CORNER_RADIUS(3),
|
||||||
|
.border = { .color = is_open ? g_theme.accent : g_theme.border, .width = { 1, 1, 1, 1 } }
|
||||||
|
) {
|
||||||
|
CLAY(WIDI(id, 500),
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
CLAY_TEXT(clay_str(current_label), &g_widget_text_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAY(WIDI(id, 501),
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIT() },
|
||||||
|
.childAlignment = { .x = CLAY_ALIGN_X_CENTER },
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
CLAY_TEXT(CLAY_STRING("v"), &g_widget_text_config_dim);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle open on click of header
|
||||||
|
if (header_hovered && g_wstate.mouse_clicked) {
|
||||||
|
if (is_open) {
|
||||||
|
g_wstate.open_dropdown_id = 0;
|
||||||
|
is_open = 0;
|
||||||
|
} else {
|
||||||
|
g_wstate.open_dropdown_id = eid.id;
|
||||||
|
is_open = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw dropdown list if open
|
||||||
|
if (is_open) {
|
||||||
|
Clay_ElementId list_id = WIDI(id, 502);
|
||||||
|
CLAY(list_id,
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIT() },
|
||||||
|
.layoutDirection = CLAY_TOP_TO_BOTTOM,
|
||||||
|
},
|
||||||
|
.backgroundColor = g_theme.bg_dark,
|
||||||
|
.border = { .color = g_theme.border, .width = { 1, 1, 1, 1 } }
|
||||||
|
) {
|
||||||
|
for (S32 i = 0; i < count; i++) {
|
||||||
|
B32 is_item_selected = (*selected == i);
|
||||||
|
Clay_ElementId item_id = WIDI(id, i + 600);
|
||||||
|
B32 item_hovered = Clay_PointerOver(item_id);
|
||||||
|
|
||||||
|
Clay_Color item_bg = is_item_selected ? g_theme.accent : g_theme.bg_dark;
|
||||||
|
if (item_hovered) item_bg = g_theme.bg_lighter;
|
||||||
|
|
||||||
|
CLAY(item_id,
|
||||||
|
.layout = {
|
||||||
|
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(28) },
|
||||||
|
.padding = { 8, 8, 0, 0 },
|
||||||
|
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
|
||||||
|
},
|
||||||
|
.backgroundColor = item_bg
|
||||||
|
) {
|
||||||
|
CLAY_TEXT(clay_str(options[i]), &g_widget_text_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item_hovered && g_wstate.mouse_clicked) {
|
||||||
|
*selected = i;
|
||||||
|
changed = 1;
|
||||||
|
g_wstate.open_dropdown_id = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close if clicked outside both header and list
|
||||||
|
if (g_wstate.mouse_clicked && !header_hovered && !Clay_PointerOver(list_id)) {
|
||||||
|
// Check if any item was clicked (already handled above)
|
||||||
|
B32 clicked_item = 0;
|
||||||
|
for (S32 i = 0; i < count; i++) {
|
||||||
|
if (Clay_PointerOver(WIDI(id, i + 600))) { clicked_item = 1; break; }
|
||||||
|
}
|
||||||
|
if (!clicked_item) {
|
||||||
|
g_wstate.open_dropdown_id = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,70 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
// ui_widgets.h - Removed: Clay handles all layout and widgets directly.
|
// ui_widgets.h - Immediate-mode widgets built on top of Clay.
|
||||||
// This file is kept empty for the unity build include order.
|
//
|
||||||
|
// Each widget function takes a unique string ID, the value to display/edit,
|
||||||
|
// and returns whether the value was changed (or the widget was activated).
|
||||||
|
// The caller owns all data — the widget layer only stores transient UI state
|
||||||
|
// like which text field is focused or which dropdown is open.
|
||||||
|
|
||||||
|
#include "ui/ui_core.h"
|
||||||
|
#include "platform/platform.h"
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Widget state (global, managed by widget layer)
|
||||||
|
|
||||||
|
#define UI_WIDGET_MAX_DROPDOWN_ITEMS 32
|
||||||
|
|
||||||
|
struct UI_WidgetState {
|
||||||
|
// Text input focus
|
||||||
|
uint32_t focused_id; // Clay element ID hash of the focused text input (0 = none)
|
||||||
|
int32_t cursor_pos; // Cursor position in focused text input
|
||||||
|
F32 cursor_blink; // Blink timer (seconds)
|
||||||
|
|
||||||
|
// Dropdown
|
||||||
|
uint32_t open_dropdown_id; // Clay element ID hash of the open dropdown (0 = none)
|
||||||
|
|
||||||
|
// Input events for this frame
|
||||||
|
PlatformInputEvents input;
|
||||||
|
|
||||||
|
// Click detection
|
||||||
|
B32 mouse_down;
|
||||||
|
B32 was_mouse_down;
|
||||||
|
B32 mouse_clicked; // true on the frame mouse transitions from up->down
|
||||||
|
};
|
||||||
|
|
||||||
|
extern UI_WidgetState g_wstate;
|
||||||
|
|
||||||
|
// Call once at startup
|
||||||
|
void ui_widgets_init();
|
||||||
|
|
||||||
|
// Call each frame before building widgets. Pass in the frame's input events.
|
||||||
|
void ui_widgets_begin_frame(PlatformInputEvents input, B32 mouse_down, B32 was_mouse_down);
|
||||||
|
|
||||||
|
// Reset per-frame text input display buffer allocator (called by begin_frame, but
|
||||||
|
// can also be called manually if needed)
|
||||||
|
void ui_text_input_reset_display_bufs();
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// Widgets
|
||||||
|
// All IDs must be unique string literals (passed to CLAY_ID internally).
|
||||||
|
|
||||||
|
// Simple label
|
||||||
|
void ui_label(const char *id, const char *text);
|
||||||
|
|
||||||
|
// Clickable button. Returns true on the frame it was clicked.
|
||||||
|
B32 ui_button(const char *id, const char *text);
|
||||||
|
|
||||||
|
// Checkbox. Toggles *value on click. Returns true if value changed.
|
||||||
|
B32 ui_checkbox(const char *id, const char *label, B32 *value);
|
||||||
|
|
||||||
|
// Radio button group. Sets *selected to the clicked index. Returns true if changed.
|
||||||
|
// options is an array of label strings, count is the number of options.
|
||||||
|
B32 ui_radio_group(const char *id, const char **options, S32 count, S32 *selected);
|
||||||
|
|
||||||
|
// Single-line text input. Edits buf in-place (null-terminated, max buf_size-1 chars).
|
||||||
|
// Returns true if the text changed this frame.
|
||||||
|
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user