Update readme, add potentiometer controls
This commit is contained in:
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