Update readme, add potentiometer controls
This commit is contained in:
30
README.md
30
README.md
@@ -47,11 +47,29 @@ Abstracts window creation, event polling, menus, clipboard, and native handles b
|
||||
|
||||
A thin wrapper around [Clay](https://github.com/nicbarker/clay) (v0.14), a single-header C layout library. Clay uses macros (`CLAY()`, `CLAY_TEXT()`, etc.) for declarative layout with automatic sizing, flex-like child arrangement, and built-in text measurement.
|
||||
|
||||
- `ui_core.h` / `ui_core.cpp` — Defines `UI_Context` and `UI_Theme`, handles Clay initialization, lifecycle (`ui_begin_frame` / `ui_end_frame`), text measurement bridge, and error handling. `CLAY_IMPLEMENTATION` is defined here.
|
||||
- `ui_widgets.h` / `ui_widgets.cpp` — Widget abstractions (currently stubs, reserved for future use).
|
||||
- `ui_core.h` / `ui_core.cpp` — Defines `UI_Context` and `UI_Theme`, handles Clay initialization, lifecycle (`ui_begin_frame` / `ui_end_frame`), text measurement bridge, theme/accent management, and error handling. `CLAY_IMPLEMENTATION` is defined here.
|
||||
- `ui_widgets.h` / `ui_widgets.cpp` — Immediate-mode widget library: buttons, checkboxes, radio groups, text inputs (with selection, clipboard, Tab cycling), dropdowns, tab bars, draggable windows, and modal dialogs.
|
||||
- `ui_icons.h` / `ui_icons.cpp` — SVG icon atlas rasterized at startup via lunasvg. Icons are rendered as custom Clay elements.
|
||||
|
||||
The application layout is built in `main.cpp` using Clay macros directly. Panel builder functions (`build_browser_panel`, `build_main_panel`, etc.) compose the UI each frame.
|
||||
|
||||
#### Theme system
|
||||
|
||||
The UI supports a full theme system managed through `UI_Theme` (`ui_core.h`) with live switching at runtime:
|
||||
|
||||
- **Base themes**: Dark and Light, selectable via `ui_set_theme()`. Each defines a full palette — backgrounds, borders, text, accent colors, title bars, scrollbars, tab colors, and drop shadow opacity.
|
||||
- **Accent palettes**: 7 color options (Blue, Turquoise, Orange, Purple, Pink, Red, Green) applied via `ui_set_accent()`. Each palette provides coordinated accent, hover, tab gradient, button text, and tab text colors tuned for both dark and light base themes.
|
||||
- **Corner radius**: Configurable at runtime (None / Small / Medium / Large) via `g_theme.corner_radius`. All widgets read `CORNER_RADIUS` which scales with `uis()`.
|
||||
- **UI scale**: Cmd+/Cmd- (Ctrl on Windows) zoom from 0.5x to 3.0x. All widget sizes, padding, font sizes, and corner radii scale via `uis()` / `uip()` / `uifs()` helpers.
|
||||
|
||||
#### Visual depth
|
||||
|
||||
Interactive elements use subtle visual effects for a DAW-style look:
|
||||
|
||||
- **Gradients**: Buttons, title bars, dropdown headers, and tab bars use vertical gradients via Clay's `CLAY_RENDER_COMMAND_TYPE_CUSTOM` system and a per-frame `CustomGradientData` pool. Buttons and title bars are lighter on top (raised), text inputs are darker on top (inset/recessed).
|
||||
- **Drop shadows**: Floating windows, modals, and dropdown lists cast soft multi-layer shadows. 7 concentric rects with linearly decreasing opacity simulate a gaussian blur, offset down-right for directional lighting.
|
||||
- **Panel highlights**: 1px lighter lines at the top of panel content areas simulate beveled edges.
|
||||
|
||||
### Renderer (`src/renderer/`)
|
||||
|
||||
Custom SDF-based pipeline for UI rendering. Processes Clay's `Clay_RenderCommandArray` output directly — no intermediate scene graph.
|
||||
@@ -102,9 +120,10 @@ src/
|
||||
renderer_dx12.cpp DirectX 12 implementation
|
||||
renderer_metal.mm Metal implementation
|
||||
ui/
|
||||
ui_core.h Clay wrapper types and lifecycle API
|
||||
ui_core.cpp Clay init, text measurement bridge, theme
|
||||
ui_widgets.h / .cpp Widget stubs (reserved)
|
||||
ui_core.h Clay wrapper, UI_Theme, scale helpers, custom render types
|
||||
ui_core.cpp Clay init, text measurement bridge, theme/accent palettes
|
||||
ui_widgets.h / .cpp Immediate-mode widgets (button, checkbox, radio, text input, dropdown, tab bar, window, modal)
|
||||
ui_icons.h / .cpp SVG icon atlas (check, close, chevron) via lunasvg
|
||||
audio/
|
||||
audio.h Audio device and playback API
|
||||
audio_asio.cpp Windows ASIO implementation
|
||||
@@ -142,3 +161,4 @@ All dependencies are vendored as source. On Windows, nothing to install beyond t
|
||||
|
||||
- [nob.h](https://github.com/tsoding/nob.h) — build system
|
||||
- [Clay](https://github.com/nicbarker/clay) — single-header C layout library (v0.14, with MSVC C++ patches)
|
||||
- [LunaSVG](https://github.com/nicbarker/lunasvg) — SVG rendering library (MIT, by Samuel Ugochukwu) used to rasterize icon SVGs into an R8 texture atlas at startup. Icons (close, check, chevron, knob) are defined as inline SVG strings in `ui_icons.cpp` and rendered at a fixed pixel size into a packed atlas. The atlas is uploaded to the GPU once and sampled by the renderer for icon and rotated-icon custom elements. Bundled with [PlutoVG](https://github.com/nicbarker/plutovg) as its 2D vector graphics backend.
|
||||
|
||||
24
src/main.cpp
24
src/main.cpp
@@ -106,6 +106,10 @@ struct AppState {
|
||||
// Corner radius selection
|
||||
S32 radius_sel;
|
||||
|
||||
// Knob demo state
|
||||
F32 demo_knob_unsigned;
|
||||
F32 demo_knob_signed;
|
||||
|
||||
// Audio device selection
|
||||
S32 audio_device_sel; // dropdown index: 0 = None, 1+ = device
|
||||
S32 audio_device_prev; // previous selection for change detection
|
||||
@@ -278,6 +282,24 @@ static void build_main_panel(AppState *app) {
|
||||
.backgroundColor = g_theme.border
|
||||
) {}
|
||||
|
||||
// Section: Knobs
|
||||
ui_label("LblKnobs", "Knobs");
|
||||
CLAY(CLAY_ID("KnobRow"),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
|
||||
.childGap = uip(16),
|
||||
.layoutDirection = CLAY_LEFT_TO_RIGHT,
|
||||
}
|
||||
) {
|
||||
ui_knob("KnobVolume", "Volume", &app->demo_knob_unsigned, 100.0f, 0, 75.0f, 1);
|
||||
ui_knob("KnobPan", "Pan", &app->demo_knob_signed, 50.0f, 1, 0.0f, 1);
|
||||
}
|
||||
|
||||
CLAY(CLAY_ID("Sep6"),
|
||||
.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"),
|
||||
@@ -876,6 +898,8 @@ int main(int argc, char **argv) {
|
||||
app.show_props = 1;
|
||||
app.show_log = 1;
|
||||
app.show_midi_devices = 1;
|
||||
app.demo_knob_unsigned = 75.0f;
|
||||
app.demo_knob_signed = 0.0f;
|
||||
app.demo_dropdown_sel = 1; // default to 48000 Hz
|
||||
app.radius_sel = 1; // default to "Small" (4.0f)
|
||||
snprintf(app.demo_text_a, sizeof(app.demo_text_a), "My Instrument");
|
||||
|
||||
@@ -15,6 +15,7 @@ enum {
|
||||
kVK_RightShift = 0x3C, kVK_RightCommand = 0x36,
|
||||
kVK_ANSI_Equal = 0x18, kVK_ANSI_Minus = 0x1B,
|
||||
kVK_ANSI_0 = 0x1D,
|
||||
kVK_ANSI_KeypadEnter = 0x4C,
|
||||
};
|
||||
|
||||
static uint8_t macos_keycode_to_pkey(uint16_t keycode) {
|
||||
@@ -24,6 +25,7 @@ static uint8_t macos_keycode_to_pkey(uint16_t keycode) {
|
||||
case kVK_ANSI_V: return PKEY_V;
|
||||
case kVK_ANSI_X: return PKEY_X;
|
||||
case kVK_Return: return PKEY_RETURN;
|
||||
case kVK_ANSI_KeypadEnter: return PKEY_RETURN;
|
||||
case kVK_Tab: return PKEY_TAB;
|
||||
case kVK_Delete: return PKEY_BACKSPACE;
|
||||
case kVK_ForwardDelete:return PKEY_DELETE;
|
||||
@@ -399,6 +401,11 @@ PlatformInput platform_get_input(PlatformWindow *window) {
|
||||
result.mouse_down = window->mouse_down_state;
|
||||
window->prev_mouse_down = result.mouse_down;
|
||||
|
||||
// Poll current modifier state (so shift/ctrl are accurate even without key events)
|
||||
NSEventModifierFlags mods = [NSEvent modifierFlags];
|
||||
result.ctrl_held = (mods & NSEventModifierFlagCommand) != 0;
|
||||
result.shift_held = (mods & NSEventModifierFlagShift) != 0;
|
||||
|
||||
// Clear accumulated events for next frame
|
||||
window->input = {};
|
||||
return result;
|
||||
|
||||
@@ -870,6 +870,61 @@ static void emit_quad(DrawBatch *batch,
|
||||
batch->index_count += 6;
|
||||
}
|
||||
|
||||
static void emit_quad_rotated(DrawBatch *batch,
|
||||
float x0, float y0, float x1, float y1,
|
||||
float u0, float v0, float u1, float v1,
|
||||
float cr, float cg, float cb, float ca,
|
||||
float angle_rad)
|
||||
{
|
||||
if (batch->vertex_count + 4 > MAX_VERTICES || batch->index_count + 6 > MAX_INDICES)
|
||||
return;
|
||||
|
||||
U32 base = batch->vertex_count;
|
||||
UIVertex *v = &batch->vertices[base];
|
||||
|
||||
float cx = (x0 + x1) * 0.5f;
|
||||
float cy = (y0 + y1) * 0.5f;
|
||||
float cosA = cosf(angle_rad);
|
||||
float sinA = sinf(angle_rad);
|
||||
|
||||
float dx0 = x0 - cx, dy0 = y0 - cy;
|
||||
float dx1 = x1 - cx, dy1 = y1 - cy;
|
||||
|
||||
v[0].pos[0] = cx + dx0 * cosA - dy0 * sinA;
|
||||
v[0].pos[1] = cy + dx0 * sinA + dy0 * cosA;
|
||||
v[0].uv[0] = u0; v[0].uv[1] = v0;
|
||||
|
||||
v[1].pos[0] = cx + dx1 * cosA - dy0 * sinA;
|
||||
v[1].pos[1] = cy + dx1 * sinA + dy0 * cosA;
|
||||
v[1].uv[0] = u1; v[1].uv[1] = v0;
|
||||
|
||||
v[2].pos[0] = cx + dx1 * cosA - dy1 * sinA;
|
||||
v[2].pos[1] = cy + dx1 * sinA + dy1 * cosA;
|
||||
v[2].uv[0] = u1; v[2].uv[1] = v1;
|
||||
|
||||
v[3].pos[0] = cx + dx0 * cosA - dy1 * sinA;
|
||||
v[3].pos[1] = cy + dx0 * sinA + dy1 * cosA;
|
||||
v[3].uv[0] = u0; v[3].uv[1] = v1;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
v[i].col[0] = cr; v[i].col[1] = cg; v[i].col[2] = cb; v[i].col[3] = ca;
|
||||
v[i].rect_min[0] = 0; v[i].rect_min[1] = 0;
|
||||
v[i].rect_max[0] = 0; v[i].rect_max[1] = 0;
|
||||
v[i].corner_radii[0] = 0; v[i].corner_radii[1] = 0;
|
||||
v[i].corner_radii[2] = 0; v[i].corner_radii[3] = 0;
|
||||
v[i].border_thickness = 0;
|
||||
v[i].softness = 0;
|
||||
v[i].mode = 1.0f;
|
||||
}
|
||||
|
||||
U32 *idx = &batch->indices[batch->index_count];
|
||||
idx[0] = base; idx[1] = base + 1; idx[2] = base + 2;
|
||||
idx[3] = base; idx[4] = base + 2; idx[5] = base + 3;
|
||||
|
||||
batch->vertex_count += 4;
|
||||
batch->index_count += 6;
|
||||
}
|
||||
|
||||
static void emit_rect(DrawBatch *batch,
|
||||
float x0, float y0, float x1, float y1,
|
||||
float cr, float cg, float cb, float ca,
|
||||
@@ -1276,6 +1331,18 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 1.0f);
|
||||
} else if (type == CUSTOM_RENDER_ROTATED_ICON) {
|
||||
bind_icon();
|
||||
CustomRotatedIconData *ri = (CustomRotatedIconData *)custom->customData;
|
||||
Clay_Color c = ri->color;
|
||||
float cr = c.r / 255.f, cg = c.g / 255.f;
|
||||
float cb = c.b / 255.f, ca = c.a / 255.f;
|
||||
UI_IconInfo *info = &g_icons[ri->icon_id];
|
||||
emit_quad_rotated(&batch,
|
||||
bb.x, bb.y, bb.x + bb.width, bb.y + bb.height,
|
||||
info->u0, info->v0, info->u1, info->v1,
|
||||
cr, cg, cb, ca,
|
||||
ri->angle_rad);
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
@@ -401,6 +401,63 @@ static void emit_quad(DrawBatch *batch,
|
||||
batch->index_count += 6;
|
||||
}
|
||||
|
||||
static void emit_quad_rotated(DrawBatch *batch,
|
||||
float x0, float y0, float x1, float y1,
|
||||
float u0, float v0, float u1, float v1,
|
||||
float cr, float cg, float cb, float ca,
|
||||
float angle_rad)
|
||||
{
|
||||
if (batch->vertex_count + 4 > MAX_VERTICES || batch->index_count + 6 > MAX_INDICES)
|
||||
return;
|
||||
|
||||
U32 base = batch->vertex_count;
|
||||
UIVertex *v = &batch->vertices[base];
|
||||
|
||||
float cx = (x0 + x1) * 0.5f;
|
||||
float cy = (y0 + y1) * 0.5f;
|
||||
float cosA = cosf(angle_rad);
|
||||
float sinA = sinf(angle_rad);
|
||||
|
||||
// Corner offsets from center
|
||||
float dx0 = x0 - cx, dy0 = y0 - cy;
|
||||
float dx1 = x1 - cx, dy1 = y1 - cy;
|
||||
|
||||
// Rotate each corner around center
|
||||
v[0].pos[0] = cx + dx0 * cosA - dy0 * sinA;
|
||||
v[0].pos[1] = cy + dx0 * sinA + dy0 * cosA;
|
||||
v[0].uv[0] = u0; v[0].uv[1] = v0;
|
||||
|
||||
v[1].pos[0] = cx + dx1 * cosA - dy0 * sinA;
|
||||
v[1].pos[1] = cy + dx1 * sinA + dy0 * cosA;
|
||||
v[1].uv[0] = u1; v[1].uv[1] = v0;
|
||||
|
||||
v[2].pos[0] = cx + dx1 * cosA - dy1 * sinA;
|
||||
v[2].pos[1] = cy + dx1 * sinA + dy1 * cosA;
|
||||
v[2].uv[0] = u1; v[2].uv[1] = v1;
|
||||
|
||||
v[3].pos[0] = cx + dx0 * cosA - dy1 * sinA;
|
||||
v[3].pos[1] = cy + dx0 * sinA + dy1 * cosA;
|
||||
v[3].uv[0] = u0; v[3].uv[1] = v1;
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
v[i].col[0] = cr; v[i].col[1] = cg; v[i].col[2] = cb; v[i].col[3] = ca;
|
||||
v[i].rect_min[0] = 0; v[i].rect_min[1] = 0;
|
||||
v[i].rect_max[0] = 0; v[i].rect_max[1] = 0;
|
||||
v[i].corner_radii[0] = 0; v[i].corner_radii[1] = 0;
|
||||
v[i].corner_radii[2] = 0; v[i].corner_radii[3] = 0;
|
||||
v[i].border_thickness = 0;
|
||||
v[i].softness = 0;
|
||||
v[i].mode = 1.0f; // textured
|
||||
}
|
||||
|
||||
U32 *idx = &batch->indices[batch->index_count];
|
||||
idx[0] = base; idx[1] = base + 1; idx[2] = base + 2;
|
||||
idx[3] = base; idx[4] = base + 2; idx[5] = base + 3;
|
||||
|
||||
batch->vertex_count += 4;
|
||||
batch->index_count += 6;
|
||||
}
|
||||
|
||||
static void emit_rect(DrawBatch *batch,
|
||||
float x0, float y0, float x1, float y1,
|
||||
float cr, float cg, float cb, float ca,
|
||||
@@ -845,6 +902,18 @@ void renderer_end_frame(Renderer *r, Clay_RenderCommandArray render_commands) {
|
||||
0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
0, 0, 1.0f);
|
||||
} else if (type == CUSTOM_RENDER_ROTATED_ICON) {
|
||||
bind_icon_texture();
|
||||
CustomRotatedIconData *ri = (CustomRotatedIconData *)custom->customData;
|
||||
Clay_Color c = ri->color;
|
||||
float cr = c.r / 255.f, cg = c.g / 255.f;
|
||||
float cb = c.b / 255.f, ca = c.a / 255.f;
|
||||
UI_IconInfo *info = &g_icons[ri->icon_id];
|
||||
emit_quad_rotated(&batch,
|
||||
bb.x, bb.y, bb.x + bb.width, bb.y + bb.height,
|
||||
info->u0, info->v0, info->u1, info->v1,
|
||||
cr, cg, cb, ca,
|
||||
ri->angle_rad);
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
@@ -118,6 +118,7 @@ static inline uint16_t uifs(float x) { return (uint16_t)(x * g_ui_scale + 0.5f);
|
||||
enum CustomRenderType {
|
||||
CUSTOM_RENDER_VGRADIENT = 1,
|
||||
CUSTOM_RENDER_ICON = 2,
|
||||
CUSTOM_RENDER_ROTATED_ICON = 3,
|
||||
};
|
||||
|
||||
struct CustomGradientData {
|
||||
@@ -132,6 +133,13 @@ struct CustomIconData {
|
||||
Clay_Color color;
|
||||
};
|
||||
|
||||
struct CustomRotatedIconData {
|
||||
CustomRenderType type; // CUSTOM_RENDER_ROTATED_ICON
|
||||
S32 icon_id;
|
||||
Clay_Color color;
|
||||
F32 angle_rad;
|
||||
};
|
||||
|
||||
////////////////////////////////
|
||||
// Font sizes
|
||||
|
||||
@@ -149,6 +157,8 @@ struct CustomIconData {
|
||||
#define WIDGET_INPUT_HEIGHT uis(30)
|
||||
#define WIDGET_DROPDOWN_HEIGHT uis(30)
|
||||
#define WIDGET_DROPDOWN_ITEM_H uis(28)
|
||||
#define WIDGET_KNOB_SIZE uis(48)
|
||||
#define WIDGET_KNOB_LABEL_GAP uip(4)
|
||||
|
||||
////////////////////////////////
|
||||
// Corner radius (from theme)
|
||||
|
||||
@@ -23,6 +23,12 @@ static const char *g_icon_svgs[UI_ICON_COUNT] = {
|
||||
R"(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M7 10 L12 15 L17 10" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>)",
|
||||
|
||||
// UI_ICON_KNOB - filled circle with indicator line pointing up (12 o'clock)
|
||||
R"(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" fill="white" opacity="0.25"/>
|
||||
<line x1="12" y1="12" x2="12" y2="3" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>)",
|
||||
};
|
||||
|
||||
U8 *ui_icons_rasterize_atlas(S32 *out_w, S32 *out_h, S32 icon_size) {
|
||||
|
||||
@@ -7,6 +7,7 @@ enum UI_IconID {
|
||||
UI_ICON_CLOSE,
|
||||
UI_ICON_CHECK,
|
||||
UI_ICON_CHEVRON_DOWN,
|
||||
UI_ICON_KNOB,
|
||||
UI_ICON_COUNT
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include "ui/ui_widgets.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
|
||||
UI_WidgetState g_wstate = {};
|
||||
|
||||
@@ -20,6 +22,16 @@ static S32 g_icon_pool_count = 0;
|
||||
static CustomGradientData g_grad_pool[UI_MAX_GRADIENTS_PER_FRAME];
|
||||
static S32 g_grad_pool_count = 0;
|
||||
|
||||
// Rotated icon per-frame pool (for knobs)
|
||||
#define UI_MAX_ROTATED_ICONS_PER_FRAME 16
|
||||
static CustomRotatedIconData g_rotated_icon_pool[UI_MAX_ROTATED_ICONS_PER_FRAME];
|
||||
static S32 g_rotated_icon_pool_count = 0;
|
||||
|
||||
// Static buffer pool for knob value text
|
||||
#define UI_MAX_KNOB_TEXT_BUFS 8
|
||||
static char g_knob_text_bufs[UI_MAX_KNOB_TEXT_BUFS][32];
|
||||
static S32 g_knob_text_buf_count = 0;
|
||||
|
||||
static CustomGradientData *alloc_gradient(Clay_Color top, Clay_Color bottom) {
|
||||
if (g_grad_pool_count >= UI_MAX_GRADIENTS_PER_FRAME) return nullptr;
|
||||
CustomGradientData *g = &g_grad_pool[g_grad_pool_count++];
|
||||
@@ -32,6 +44,9 @@ static CustomGradientData *alloc_gradient(Clay_Color top, Clay_Color bottom) {
|
||||
// Per-frame shadow layer ID counter (each shadow uses N unique IDs)
|
||||
static S32 g_shadow_id_counter = 0;
|
||||
|
||||
// Frame counter for double-click detection
|
||||
static S32 g_frame_number = 0;
|
||||
|
||||
// Emit a smooth multi-layer drop shadow as floating rects.
|
||||
// bb: bounding box of the element to shadow (previous frame)
|
||||
// ox/oy: directional offset (light direction)
|
||||
@@ -113,7 +128,15 @@ void ui_widgets_begin_frame(PlatformInput input) {
|
||||
g_wstate.mouse_clicked = (input.mouse_down && !input.was_mouse_down);
|
||||
g_icon_pool_count = 0;
|
||||
g_grad_pool_count = 0;
|
||||
g_rotated_icon_pool_count = 0;
|
||||
g_knob_text_buf_count = 0;
|
||||
g_shadow_id_counter = 0;
|
||||
g_frame_number++;
|
||||
|
||||
// Release knob drag if mouse is up
|
||||
if (!input.mouse_down && g_wstate.knob_drag.dragging_id != 0) {
|
||||
g_wstate.knob_drag.dragging_id = 0;
|
||||
}
|
||||
g_wstate.cursor_blink += 1.0f / 60.0f;
|
||||
g_wstate.text_input_count = 0;
|
||||
g_wstate.tab_pressed = 0;
|
||||
@@ -1356,3 +1379,494 @@ S32 ui_tab_bar(const char *id, const char **labels, S32 count, S32 *selected) {
|
||||
}
|
||||
return *selected;
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// Knob / Potentiometer
|
||||
|
||||
B32 ui_knob(const char *id, const char *label, F32 *value, F32 max_val, B32 is_signed, F32 default_val, B32 editable) {
|
||||
ensure_widget_text_configs();
|
||||
|
||||
F32 knob_size = WIDGET_KNOB_SIZE;
|
||||
B32 changed = 0;
|
||||
|
||||
// Normalize value to [0,1]
|
||||
F32 normalized;
|
||||
if (is_signed) {
|
||||
normalized = (*value + max_val) / (2.0f * max_val);
|
||||
} else {
|
||||
normalized = *value / max_val;
|
||||
}
|
||||
if (normalized < 0.0f) normalized = 0.0f;
|
||||
if (normalized > 1.0f) normalized = 1.0f;
|
||||
|
||||
// Angle: 270-degree sweep, -135 to +135 degrees
|
||||
F32 deg_to_rad = 3.14159265f / 180.0f;
|
||||
F32 angle_rad = (-135.0f + normalized * 270.0f) * deg_to_rad;
|
||||
|
||||
// Hash the ID for drag state tracking
|
||||
uint32_t knob_hash = Clay__HashString(clay_str(id), 0).id;
|
||||
|
||||
// Text edit state
|
||||
B32 is_editing = (editable && g_wstate.knob_edit_id == knob_hash);
|
||||
|
||||
// Drag interaction (only when not text-editing)
|
||||
UI_KnobDragState *kd = &g_wstate.knob_drag;
|
||||
|
||||
if (!is_editing && kd->dragging_id == knob_hash && g_wstate.input.mouse_down) {
|
||||
// Re-anchor when shift state changes so there's no jump
|
||||
B32 shift_now = g_wstate.input.shift_held;
|
||||
if (shift_now != kd->was_shift) {
|
||||
kd->drag_start_y = g_wstate.input.mouse_pos.y;
|
||||
kd->value_at_start = *value;
|
||||
kd->was_shift = shift_now;
|
||||
}
|
||||
|
||||
// Continue drag: vertical mouse delta mapped to value range
|
||||
// Hold Shift for fine control (5x slower)
|
||||
F32 dy = kd->drag_start_y - g_wstate.input.mouse_pos.y; // up = positive
|
||||
F32 sensitivity = 200.0f * g_ui_scale; // pixels for full range
|
||||
if (shift_now) sensitivity *= 5.0f;
|
||||
F32 range = is_signed ? (2.0f * max_val) : max_val;
|
||||
F32 new_val = kd->value_at_start + (dy / sensitivity) * range;
|
||||
|
||||
// Clamp
|
||||
F32 lo = is_signed ? -max_val : 0.0f;
|
||||
if (new_val < lo) new_val = lo;
|
||||
if (new_val > max_val) new_val = max_val;
|
||||
|
||||
if (new_val != *value) {
|
||||
*value = new_val;
|
||||
changed = 1;
|
||||
}
|
||||
|
||||
// Recalculate normalized/angle after drag
|
||||
if (is_signed) {
|
||||
normalized = (*value + max_val) / (2.0f * max_val);
|
||||
} else {
|
||||
normalized = *value / max_val;
|
||||
}
|
||||
if (normalized < 0.0f) normalized = 0.0f;
|
||||
if (normalized > 1.0f) normalized = 1.0f;
|
||||
angle_rad = (-135.0f + normalized * 270.0f) * deg_to_rad;
|
||||
}
|
||||
|
||||
// Handle text edit keyboard input before layout
|
||||
if (is_editing) {
|
||||
char *ebuf = g_wstate.knob_edit_buf;
|
||||
S32 elen = (S32)strlen(ebuf);
|
||||
S32 *ecur = &g_wstate.knob_edit_cursor;
|
||||
S32 *esel0 = &g_wstate.knob_edit_sel_start;
|
||||
S32 *esel1 = &g_wstate.knob_edit_sel_end;
|
||||
B32 commit = 0;
|
||||
B32 cancel = 0;
|
||||
B32 ctrl = g_wstate.input.ctrl_held;
|
||||
|
||||
// Clamp
|
||||
if (*ecur > elen) *ecur = elen;
|
||||
if (*esel0 > elen) *esel0 = elen;
|
||||
if (*esel1 > elen) *esel1 = elen;
|
||||
|
||||
// Selection helpers (local lambdas via inline)
|
||||
#define KE_HAS_SEL() (*esel0 != *esel1)
|
||||
#define KE_SEL_LO() (*esel0 < *esel1 ? *esel0 : *esel1)
|
||||
#define KE_SEL_HI() (*esel0 < *esel1 ? *esel1 : *esel0)
|
||||
#define KE_CLEAR_SEL() do { *esel0 = *ecur; *esel1 = *ecur; } while(0)
|
||||
|
||||
// Delete selection, returns new length
|
||||
auto ke_delete_sel = [&]() -> S32 {
|
||||
S32 lo = KE_SEL_LO(), hi = KE_SEL_HI();
|
||||
if (lo == hi) return elen;
|
||||
memmove(&ebuf[lo], &ebuf[hi], elen - hi + 1);
|
||||
*ecur = lo;
|
||||
KE_CLEAR_SEL();
|
||||
return elen - (hi - lo);
|
||||
};
|
||||
|
||||
// Process key events
|
||||
for (S32 k = 0; k < g_wstate.input.key_count; k++) {
|
||||
uint8_t key = g_wstate.input.keys[k];
|
||||
|
||||
if (ctrl) {
|
||||
if (key == PKEY_A) {
|
||||
*esel0 = 0;
|
||||
*esel1 = elen;
|
||||
*ecur = elen;
|
||||
continue;
|
||||
}
|
||||
if (key == PKEY_C) {
|
||||
if (KE_HAS_SEL()) {
|
||||
S32 lo = KE_SEL_LO(), hi = KE_SEL_HI();
|
||||
char tmp[32];
|
||||
S32 n = hi - lo;
|
||||
if (n > 31) n = 31;
|
||||
memcpy(tmp, &ebuf[lo], n);
|
||||
tmp[n] = '\0';
|
||||
platform_clipboard_set(tmp);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (key == PKEY_X) {
|
||||
if (KE_HAS_SEL()) {
|
||||
S32 lo = KE_SEL_LO(), hi = KE_SEL_HI();
|
||||
char tmp[32];
|
||||
S32 n = hi - lo;
|
||||
if (n > 31) n = 31;
|
||||
memcpy(tmp, &ebuf[lo], n);
|
||||
tmp[n] = '\0';
|
||||
platform_clipboard_set(tmp);
|
||||
elen = ke_delete_sel();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (key == PKEY_V) {
|
||||
const char *clip = platform_clipboard_get();
|
||||
if (clip) {
|
||||
if (KE_HAS_SEL()) elen = ke_delete_sel();
|
||||
S32 clip_len = (S32)strlen(clip);
|
||||
// Filter: only keep numeric chars
|
||||
char filtered[32];
|
||||
S32 flen = 0;
|
||||
for (S32 i = 0; i < clip_len && flen < 30; i++) {
|
||||
char c = clip[i];
|
||||
if ((c >= '0' && c <= '9') || c == '.' || c == '-')
|
||||
filtered[flen++] = c;
|
||||
}
|
||||
S32 space = 30 - elen;
|
||||
if (flen > space) flen = space;
|
||||
if (flen > 0) {
|
||||
memmove(&ebuf[*ecur + flen], &ebuf[*ecur], elen - *ecur + 1);
|
||||
memcpy(&ebuf[*ecur], filtered, flen);
|
||||
*ecur += flen;
|
||||
elen += flen;
|
||||
}
|
||||
KE_CLEAR_SEL();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
continue; // ignore other ctrl combos
|
||||
}
|
||||
|
||||
if (key == PKEY_RETURN) {
|
||||
commit = 1;
|
||||
} else if (key == PKEY_ESCAPE) {
|
||||
cancel = 1;
|
||||
} else if (key == PKEY_BACKSPACE) {
|
||||
if (KE_HAS_SEL()) {
|
||||
elen = ke_delete_sel();
|
||||
} else if (*ecur > 0) {
|
||||
memmove(&ebuf[*ecur - 1], &ebuf[*ecur], elen - *ecur + 1);
|
||||
(*ecur)--;
|
||||
elen--;
|
||||
}
|
||||
KE_CLEAR_SEL();
|
||||
} else if (key == PKEY_DELETE) {
|
||||
if (KE_HAS_SEL()) {
|
||||
elen = ke_delete_sel();
|
||||
} else if (*ecur < elen) {
|
||||
memmove(&ebuf[*ecur], &ebuf[*ecur + 1], elen - *ecur);
|
||||
elen--;
|
||||
}
|
||||
KE_CLEAR_SEL();
|
||||
} else if (key == PKEY_LEFT) {
|
||||
if (KE_HAS_SEL()) {
|
||||
*ecur = KE_SEL_LO();
|
||||
} else if (*ecur > 0) {
|
||||
(*ecur)--;
|
||||
}
|
||||
KE_CLEAR_SEL();
|
||||
} else if (key == PKEY_RIGHT) {
|
||||
if (KE_HAS_SEL()) {
|
||||
*ecur = KE_SEL_HI();
|
||||
} else if (*ecur < elen) {
|
||||
(*ecur)++;
|
||||
}
|
||||
KE_CLEAR_SEL();
|
||||
}
|
||||
}
|
||||
|
||||
// Process character input (digits, minus, period)
|
||||
if (!commit && !cancel) {
|
||||
for (S32 c = 0; c < g_wstate.input.char_count; c++) {
|
||||
uint16_t ch = g_wstate.input.chars[c];
|
||||
B32 valid = (ch >= '0' && ch <= '9') || ch == '.' || ch == '-';
|
||||
if (valid) {
|
||||
if (KE_HAS_SEL()) elen = ke_delete_sel();
|
||||
if (elen < 30) {
|
||||
memmove(&ebuf[*ecur + 1], &ebuf[*ecur], elen - *ecur + 1);
|
||||
ebuf[*ecur] = (char)ch;
|
||||
(*ecur)++;
|
||||
elen++;
|
||||
}
|
||||
KE_CLEAR_SEL();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#undef KE_HAS_SEL
|
||||
#undef KE_SEL_LO
|
||||
#undef KE_SEL_HI
|
||||
#undef KE_CLEAR_SEL
|
||||
|
||||
if (commit) {
|
||||
// Parse and apply
|
||||
char *end = nullptr;
|
||||
F32 parsed = strtof(ebuf, &end);
|
||||
if (end != ebuf) {
|
||||
F32 lo = is_signed ? -max_val : 0.0f;
|
||||
if (parsed < lo) parsed = lo;
|
||||
if (parsed > max_val) parsed = max_val;
|
||||
if (parsed != *value) {
|
||||
*value = parsed;
|
||||
changed = 1;
|
||||
}
|
||||
}
|
||||
g_wstate.knob_edit_id = 0;
|
||||
is_editing = 0;
|
||||
|
||||
// Recalculate after commit
|
||||
if (is_signed) {
|
||||
normalized = (*value + max_val) / (2.0f * max_val);
|
||||
} else {
|
||||
normalized = *value / max_val;
|
||||
}
|
||||
if (normalized < 0.0f) normalized = 0.0f;
|
||||
if (normalized > 1.0f) normalized = 1.0f;
|
||||
angle_rad = (-135.0f + normalized * 270.0f) * deg_to_rad;
|
||||
} else if (cancel) {
|
||||
g_wstate.knob_edit_id = 0;
|
||||
is_editing = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Format value text (only when not editing)
|
||||
char *val_text = nullptr;
|
||||
S32 val_len = 0;
|
||||
if (!is_editing && g_knob_text_buf_count < UI_MAX_KNOB_TEXT_BUFS) {
|
||||
val_text = g_knob_text_bufs[g_knob_text_buf_count];
|
||||
if (is_signed) {
|
||||
val_len = snprintf(val_text, 32, "%+.1f", *value);
|
||||
} else {
|
||||
val_len = snprintf(val_text, 32, "%.1f", *value);
|
||||
}
|
||||
g_knob_text_buf_count++;
|
||||
}
|
||||
|
||||
// Layout: vertical column (knob → value text → label)
|
||||
CLAY(WID(id),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
|
||||
.childGap = WIDGET_KNOB_LABEL_GAP,
|
||||
.childAlignment = { .x = CLAY_ALIGN_X_CENTER },
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM,
|
||||
}
|
||||
) {
|
||||
// Knob visual: circular background with rotated icon inside
|
||||
Clay_ElementId knob_eid = CLAY_IDI("KnobBg", (int)knob_hash);
|
||||
B32 hovered = Clay_PointerOver(knob_eid);
|
||||
|
||||
CLAY(knob_eid,
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_FIXED(knob_size), .height = CLAY_SIZING_FIXED(knob_size) },
|
||||
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_CENTER },
|
||||
},
|
||||
.backgroundColor = g_theme.bg_dark,
|
||||
.cornerRadius = CLAY_CORNER_RADIUS(knob_size / 2.0f)
|
||||
) {
|
||||
// Rotated icon at 75% container size
|
||||
if (g_rotated_icon_pool_count < UI_MAX_ROTATED_ICONS_PER_FRAME) {
|
||||
S32 ri_idx = g_rotated_icon_pool_count;
|
||||
CustomRotatedIconData *rdata = &g_rotated_icon_pool[g_rotated_icon_pool_count++];
|
||||
rdata->type = CUSTOM_RENDER_ROTATED_ICON;
|
||||
rdata->icon_id = (S32)UI_ICON_KNOB;
|
||||
rdata->color = g_theme.accent;
|
||||
rdata->angle_rad = angle_rad;
|
||||
|
||||
F32 icon_size = knob_size * 0.75f;
|
||||
CLAY(CLAY_IDI("KnobIcon", ri_idx),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_FIXED(icon_size), .height = CLAY_SIZING_FIXED(icon_size) },
|
||||
},
|
||||
.custom = { .customData = rdata }
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Click on knob: double-click resets to default, single click starts drag
|
||||
if (!is_editing && hovered && g_wstate.mouse_clicked && kd->dragging_id == 0) {
|
||||
// Double-click detection: same knob within 20 frames (~333ms at 60fps)
|
||||
B32 is_double_click = (kd->last_click_id == knob_hash &&
|
||||
(g_frame_number - kd->last_click_frame) < 20);
|
||||
kd->last_click_id = knob_hash;
|
||||
kd->last_click_frame = g_frame_number;
|
||||
|
||||
if (is_double_click) {
|
||||
// Reset to default value
|
||||
if (*value != default_val) {
|
||||
*value = default_val;
|
||||
changed = 1;
|
||||
}
|
||||
// Recalculate after reset
|
||||
if (is_signed) {
|
||||
normalized = (*value + max_val) / (2.0f * max_val);
|
||||
} else {
|
||||
normalized = *value / max_val;
|
||||
}
|
||||
if (normalized < 0.0f) normalized = 0.0f;
|
||||
if (normalized > 1.0f) normalized = 1.0f;
|
||||
angle_rad = (-135.0f + normalized * 270.0f) * deg_to_rad;
|
||||
// Clear so triple-click doesn't re-trigger
|
||||
kd->last_click_id = 0;
|
||||
} else {
|
||||
// Start drag
|
||||
kd->dragging_id = knob_hash;
|
||||
kd->drag_start_y = g_wstate.input.mouse_pos.y;
|
||||
kd->value_at_start = *value;
|
||||
}
|
||||
}
|
||||
|
||||
// Value text / text edit area
|
||||
if (is_editing) {
|
||||
// Show text input for direct value entry
|
||||
Clay_ElementId edit_eid = CLAY_IDI("KnobEdit", (int)knob_hash);
|
||||
|
||||
char *ebuf = g_wstate.knob_edit_buf;
|
||||
S32 elen = (S32)strlen(ebuf);
|
||||
S32 ecur = g_wstate.knob_edit_cursor;
|
||||
S32 esel0 = g_wstate.knob_edit_sel_start;
|
||||
S32 esel1 = g_wstate.knob_edit_sel_end;
|
||||
if (ecur > elen) ecur = elen;
|
||||
if (esel0 > elen) esel0 = elen;
|
||||
if (esel1 > elen) esel1 = elen;
|
||||
S32 sel_lo = esel0 < esel1 ? esel0 : esel1;
|
||||
S32 sel_hi = esel0 < esel1 ? esel1 : esel0;
|
||||
B32 has_sel = (sel_lo != sel_hi);
|
||||
|
||||
// Display buffers for before/selected/after segments
|
||||
static char ke_dbuf_before[32];
|
||||
static char ke_dbuf_sel[32];
|
||||
static char ke_dbuf_after[32];
|
||||
|
||||
static Clay_TextElementConfig knob_edit_cfg = {};
|
||||
knob_edit_cfg.textColor = g_theme.text;
|
||||
knob_edit_cfg.fontSize = FONT_SIZE_SMALL;
|
||||
knob_edit_cfg.wrapMode = CLAY_TEXT_WRAP_NONE;
|
||||
|
||||
static Clay_TextElementConfig knob_edit_sel_cfg = {};
|
||||
knob_edit_sel_cfg.textColor = Clay_Color{255, 255, 255, 255};
|
||||
knob_edit_sel_cfg.fontSize = FONT_SIZE_SMALL;
|
||||
knob_edit_sel_cfg.wrapMode = CLAY_TEXT_WRAP_NONE;
|
||||
|
||||
CLAY(edit_eid,
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_FIXED(knob_size), .height = CLAY_SIZING_FIT() },
|
||||
.padding = { uip(2), uip(2), uip(1), uip(1) },
|
||||
.childAlignment = { .x = CLAY_ALIGN_X_CENTER },
|
||||
.layoutDirection = CLAY_LEFT_TO_RIGHT,
|
||||
},
|
||||
.backgroundColor = g_theme.bg_dark,
|
||||
.cornerRadius = CLAY_CORNER_RADIUS(uis(2)),
|
||||
.border = { .color = g_theme.accent, .width = { 1, 1, 1, 1 } }
|
||||
) {
|
||||
if (has_sel) {
|
||||
// Three segments: before | selected | after
|
||||
if (sel_lo > 0) {
|
||||
S32 n = sel_lo;
|
||||
memcpy(ke_dbuf_before, ebuf, n);
|
||||
ke_dbuf_before[n] = '\0';
|
||||
Clay_String s_before = { .length = n, .chars = ke_dbuf_before };
|
||||
CLAY_TEXT(s_before, &knob_edit_cfg);
|
||||
}
|
||||
{
|
||||
S32 n = sel_hi - sel_lo;
|
||||
memcpy(ke_dbuf_sel, &ebuf[sel_lo], n);
|
||||
ke_dbuf_sel[n] = '\0';
|
||||
Clay_String s_sel = { .length = n, .chars = ke_dbuf_sel };
|
||||
CLAY(CLAY_IDI("KnobEditSel", (int)knob_hash),
|
||||
.layout = { .sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() } },
|
||||
.backgroundColor = g_theme.accent
|
||||
) {
|
||||
CLAY_TEXT(s_sel, &knob_edit_sel_cfg);
|
||||
}
|
||||
}
|
||||
if (sel_hi < elen) {
|
||||
S32 n = elen - sel_hi;
|
||||
memcpy(ke_dbuf_after, &ebuf[sel_hi], n);
|
||||
ke_dbuf_after[n] = '\0';
|
||||
Clay_String s_after = { .length = n, .chars = ke_dbuf_after };
|
||||
CLAY_TEXT(s_after, &knob_edit_cfg);
|
||||
}
|
||||
} else {
|
||||
// No selection: show text with '|' cursor
|
||||
static char knob_edit_display[64];
|
||||
S32 di = 0;
|
||||
for (S32 i = 0; i < elen + 1 && di < 62; i++) {
|
||||
if (i == ecur) knob_edit_display[di++] = '|';
|
||||
if (i < elen) knob_edit_display[di++] = ebuf[i];
|
||||
}
|
||||
knob_edit_display[di] = '\0';
|
||||
Clay_String s_cursor = { .length = di, .chars = knob_edit_display };
|
||||
CLAY_TEXT(s_cursor, &knob_edit_cfg);
|
||||
}
|
||||
}
|
||||
|
||||
// Click away from edit box → commit
|
||||
if (g_wstate.mouse_clicked && !Clay_PointerOver(edit_eid)) {
|
||||
char *end = nullptr;
|
||||
F32 parsed = strtof(g_wstate.knob_edit_buf, &end);
|
||||
if (end != g_wstate.knob_edit_buf) {
|
||||
F32 lo = is_signed ? -max_val : 0.0f;
|
||||
if (parsed < lo) parsed = lo;
|
||||
if (parsed > max_val) parsed = max_val;
|
||||
if (parsed != *value) {
|
||||
*value = parsed;
|
||||
changed = 1;
|
||||
}
|
||||
}
|
||||
g_wstate.knob_edit_id = 0;
|
||||
}
|
||||
} else {
|
||||
// Static value text display
|
||||
Clay_ElementId val_eid = CLAY_IDI("KnobVal", (int)knob_hash);
|
||||
B32 val_hovered = Clay_PointerOver(val_eid);
|
||||
|
||||
if (val_text && val_len > 0) {
|
||||
Clay_String val_str = { .isStaticallyAllocated = false, .length = val_len, .chars = val_text };
|
||||
static Clay_TextElementConfig knob_val_cfg = {};
|
||||
knob_val_cfg.textColor = g_theme.text;
|
||||
knob_val_cfg.fontSize = FONT_SIZE_SMALL;
|
||||
knob_val_cfg.wrapMode = CLAY_TEXT_WRAP_NONE;
|
||||
|
||||
CLAY(val_eid,
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_FIXED(knob_size), .height = CLAY_SIZING_FIT() },
|
||||
.padding = { uip(2), uip(2), uip(1), uip(1) },
|
||||
.childAlignment = { .x = CLAY_ALIGN_X_CENTER },
|
||||
}
|
||||
) {
|
||||
CLAY_TEXT(val_str, &knob_val_cfg);
|
||||
}
|
||||
|
||||
// Click on value text → enter edit mode with select-all
|
||||
if (editable && val_hovered && g_wstate.mouse_clicked) {
|
||||
g_wstate.knob_edit_id = knob_hash;
|
||||
g_wstate.focused_id = 0; // unfocus any text input
|
||||
// Seed buffer with current value
|
||||
if (is_signed) {
|
||||
snprintf(g_wstate.knob_edit_buf, sizeof(g_wstate.knob_edit_buf), "%+.1f", *value);
|
||||
} else {
|
||||
snprintf(g_wstate.knob_edit_buf, sizeof(g_wstate.knob_edit_buf), "%.1f", *value);
|
||||
}
|
||||
S32 slen = (S32)strlen(g_wstate.knob_edit_buf);
|
||||
g_wstate.knob_edit_cursor = slen;
|
||||
g_wstate.knob_edit_sel_start = 0;
|
||||
g_wstate.knob_edit_sel_end = slen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Label
|
||||
CLAY_TEXT(clay_str(label), &g_widget_text_config_dim);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,15 @@ struct UI_WindowSlot {
|
||||
int16_t z_order;
|
||||
};
|
||||
|
||||
struct UI_KnobDragState {
|
||||
uint32_t dragging_id; // Hash of the knob being dragged (0 = none)
|
||||
F32 drag_start_y; // Mouse Y when drag started
|
||||
F32 value_at_start; // Value when drag started
|
||||
B32 was_shift; // Shift state last frame (to re-anchor on change)
|
||||
uint32_t last_click_id; // Knob hash of last click (for double-click detection)
|
||||
S32 last_click_frame; // Frame number of last click
|
||||
};
|
||||
|
||||
struct UI_DragState {
|
||||
uint32_t dragging_id; // Window ID currently being dragged (0 = none)
|
||||
Vec2F32 drag_anchor; // Mouse position when drag started
|
||||
@@ -69,6 +78,16 @@ struct UI_WidgetState {
|
||||
S32 window_count;
|
||||
int16_t next_z;
|
||||
UI_DragState drag;
|
||||
|
||||
// Knob drag state
|
||||
UI_KnobDragState knob_drag;
|
||||
|
||||
// Knob text edit state
|
||||
uint32_t knob_edit_id; // Hash of knob in text edit mode (0 = none)
|
||||
char knob_edit_buf[32]; // Text buffer for numeric entry
|
||||
S32 knob_edit_cursor; // Cursor position in edit buffer
|
||||
S32 knob_edit_sel_start; // Selection anchor
|
||||
S32 knob_edit_sel_end; // Selection extent
|
||||
};
|
||||
|
||||
extern UI_WidgetState g_wstate;
|
||||
@@ -130,3 +149,12 @@ 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]
|
||||
// signed (is_signed=1): value in [-max_val, +max_val]
|
||||
// default_val: value restored on double-click.
|
||||
// editable: if true, clicking the value text opens a text input for direct entry.
|
||||
// Hold Shift while dragging for fine control.
|
||||
// Returns true if value changed this frame.
|
||||
B32 ui_knob(const char *id, const char *label, F32 *value, F32 max_val, B32 is_signed, F32 default_val, B32 editable = 0);
|
||||
|
||||
Reference in New Issue
Block a user