Update readme, add potentiometer controls
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user