update widgets, add copy paste buffer
This commit is contained in:
@@ -20,6 +20,17 @@ void ui_widgets_begin_frame(PlatformInputEvents input, B32 mouse_down, B32 was_m
|
||||
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;
|
||||
g_wstate.text_input_count = 0;
|
||||
g_wstate.tab_pressed = 0;
|
||||
|
||||
// Detect Tab key press this frame
|
||||
for (S32 k = 0; k < input.key_count; k++) {
|
||||
if (input.keys[k] == PKEY_TAB) {
|
||||
g_wstate.tab_pressed = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ui_text_input_reset_display_bufs();
|
||||
}
|
||||
|
||||
@@ -28,6 +39,7 @@ void ui_widgets_begin_frame(PlatformInputEvents input, B32 mouse_down, B32 was_m
|
||||
|
||||
static Clay_TextElementConfig g_widget_text_config;
|
||||
static Clay_TextElementConfig g_widget_text_config_dim;
|
||||
static Clay_TextElementConfig g_widget_text_config_sel;
|
||||
static bool g_widget_text_configs_init = false;
|
||||
|
||||
static void ensure_widget_text_configs() {
|
||||
@@ -43,6 +55,12 @@ static void ensure_widget_text_configs() {
|
||||
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;
|
||||
|
||||
// Selected text: white on accent background (background set on parent element)
|
||||
g_widget_text_config_sel = {};
|
||||
g_widget_text_config_sel.textColor = Clay_Color{255, 255, 255, 255};
|
||||
g_widget_text_config_sel.fontSize = 15;
|
||||
g_widget_text_config_sel.wrapMode = CLAY_TEXT_WRAP_NONE;
|
||||
}
|
||||
|
||||
static Clay_String clay_str(const char *s) {
|
||||
@@ -214,39 +232,106 @@ B32 ui_radio_group(const char *id, const char **options, S32 count, S32 *selecte
|
||||
////////////////////////////////
|
||||
// Text input
|
||||
//
|
||||
// Each text input gets its own display buffer so Clay string pointers
|
||||
// Each text input gets its own display buffer(s) so Clay string pointers
|
||||
// remain valid until the end of the frame. We support up to
|
||||
// MAX_TEXT_INPUTS simultaneous text inputs per frame.
|
||||
// Each text input may need up to 3 display buffers (before/sel/after).
|
||||
|
||||
#define MAX_TEXT_INPUTS 8
|
||||
static char g_text_display_bufs[MAX_TEXT_INPUTS][512];
|
||||
#define DISPLAY_BUF_SIZE 512
|
||||
// 3 buffers per input: [0]=before selection, [1]=selected text, [2]=after selection
|
||||
static char g_text_display_bufs[MAX_TEXT_INPUTS * 3][DISPLAY_BUF_SIZE];
|
||||
static S32 g_text_display_buf_idx = 0;
|
||||
|
||||
void ui_text_input_reset_display_bufs() {
|
||||
g_text_display_buf_idx = 0;
|
||||
}
|
||||
|
||||
// Helper: get selection range in normalized order (lo <= hi)
|
||||
static void text_input_get_sel(S32 *lo, S32 *hi) {
|
||||
S32 a = g_wstate.sel_start;
|
||||
S32 b = g_wstate.sel_end;
|
||||
if (a <= b) { *lo = a; *hi = b; }
|
||||
else { *lo = b; *hi = a; }
|
||||
}
|
||||
|
||||
// Helper: true if there's an active selection
|
||||
static B32 text_input_has_sel() {
|
||||
return (g_wstate.sel_start != g_wstate.sel_end);
|
||||
}
|
||||
|
||||
// Helper: delete selected range from buf, update cursor, clear selection
|
||||
// Returns new string length.
|
||||
static S32 text_input_delete_sel(char *buf, S32 len) {
|
||||
S32 lo, hi;
|
||||
text_input_get_sel(&lo, &hi);
|
||||
if (lo == hi) return len;
|
||||
memmove(&buf[lo], &buf[hi], len - hi + 1);
|
||||
g_wstate.cursor_pos = lo;
|
||||
g_wstate.sel_start = lo;
|
||||
g_wstate.sel_end = lo;
|
||||
return len - (hi - lo);
|
||||
}
|
||||
|
||||
// Helper: copy selected text to clipboard
|
||||
static void text_input_copy_sel(const char *buf, S32 len) {
|
||||
S32 lo, hi;
|
||||
text_input_get_sel(&lo, &hi);
|
||||
if (lo == hi || lo >= len) return;
|
||||
if (hi > len) hi = len;
|
||||
// Use a temp buffer to null-terminate the selection
|
||||
char tmp[DISPLAY_BUF_SIZE];
|
||||
S32 sel_len = hi - lo;
|
||||
if (sel_len > (S32)sizeof(tmp) - 1) sel_len = (S32)sizeof(tmp) - 1;
|
||||
memcpy(tmp, &buf[lo], sel_len);
|
||||
tmp[sel_len] = '\0';
|
||||
platform_clipboard_set(tmp);
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
// Register this text input for Tab cycling
|
||||
Clay_ElementId eid = WID(id);
|
||||
if (g_wstate.text_input_count < UI_WIDGET_MAX_TEXT_INPUTS) {
|
||||
g_wstate.text_input_ids[g_wstate.text_input_count++] = eid.id;
|
||||
}
|
||||
|
||||
// Grab unique display buffers for this text input (up to 3)
|
||||
S32 my_buf_base = g_text_display_buf_idx;
|
||||
g_text_display_buf_idx += 3;
|
||||
if (my_buf_base + 2 >= MAX_TEXT_INPUTS * 3) my_buf_base = 0; // safety
|
||||
char *dbuf_before = g_text_display_bufs[my_buf_base + 0];
|
||||
char *dbuf_sel = g_text_display_bufs[my_buf_base + 1];
|
||||
char *dbuf_after = g_text_display_bufs[my_buf_base + 2];
|
||||
|
||||
B32 hovered = Clay_PointerOver(eid);
|
||||
B32 is_focused = (g_wstate.focused_id == eid.id);
|
||||
|
||||
// Tab cycling: if Tab was pressed and we're focused, move to next input.
|
||||
// We handle this BEFORE processing other input so the Tab key doesn't
|
||||
// get processed as a regular key. The actual focus change is deferred
|
||||
// to after all text inputs have registered (see end of function).
|
||||
B32 wants_tab = 0;
|
||||
if (is_focused && g_wstate.tab_pressed) {
|
||||
wants_tab = 1;
|
||||
}
|
||||
|
||||
// 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.sel_start = g_wstate.cursor_pos;
|
||||
g_wstate.sel_end = g_wstate.cursor_pos;
|
||||
g_wstate.cursor_blink = 0;
|
||||
is_focused = 1;
|
||||
} else {
|
||||
// Already focused, clicking clears selection
|
||||
g_wstate.sel_start = g_wstate.cursor_pos;
|
||||
g_wstate.sel_end = g_wstate.cursor_pos;
|
||||
}
|
||||
} else if (is_focused) {
|
||||
g_wstate.focused_id = 0;
|
||||
@@ -257,45 +342,133 @@ B32 ui_text_input(const char *id, char *buf, S32 buf_size) {
|
||||
// Process keyboard input if focused
|
||||
if (is_focused) {
|
||||
S32 len = (S32)strlen(buf);
|
||||
B32 ctrl = g_wstate.input.ctrl_held;
|
||||
|
||||
// Clamp cursor
|
||||
// Clamp cursor and selection
|
||||
if (g_wstate.cursor_pos > len) g_wstate.cursor_pos = len;
|
||||
if (g_wstate.cursor_pos < 0) g_wstate.cursor_pos = 0;
|
||||
if (g_wstate.sel_start > len) g_wstate.sel_start = len;
|
||||
if (g_wstate.sel_end > len) g_wstate.sel_end = len;
|
||||
|
||||
// Key events first (backspace generates both WM_KEYDOWN and WM_CHAR;
|
||||
// we handle backspace/delete/arrows via keys, printable chars via chars)
|
||||
// Key events
|
||||
for (S32 k = 0; k < g_wstate.input.key_count; k++) {
|
||||
uint8_t key = g_wstate.input.keys[k];
|
||||
|
||||
// Skip Tab — handled via tab cycling
|
||||
if (key == PKEY_TAB) continue;
|
||||
|
||||
// Ctrl shortcuts
|
||||
if (ctrl) {
|
||||
if (key == PKEY_A) {
|
||||
// Select all
|
||||
g_wstate.sel_start = 0;
|
||||
g_wstate.sel_end = len;
|
||||
g_wstate.cursor_pos = len;
|
||||
continue;
|
||||
}
|
||||
if (key == PKEY_C) {
|
||||
// Copy
|
||||
text_input_copy_sel(buf, len);
|
||||
continue;
|
||||
}
|
||||
if (key == PKEY_X) {
|
||||
// Cut
|
||||
if (text_input_has_sel()) {
|
||||
text_input_copy_sel(buf, len);
|
||||
len = text_input_delete_sel(buf, len);
|
||||
text_changed = 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (key == PKEY_V) {
|
||||
// Paste
|
||||
const char *clip = platform_clipboard_get();
|
||||
if (clip) {
|
||||
// Delete selection first if any
|
||||
if (text_input_has_sel()) {
|
||||
len = text_input_delete_sel(buf, len);
|
||||
}
|
||||
S32 clip_len = (S32)strlen(clip);
|
||||
// Filter to single line (stop at newline)
|
||||
for (S32 i = 0; i < clip_len; i++) {
|
||||
if (clip[i] == '\n' || clip[i] == '\r') { clip_len = i; break; }
|
||||
}
|
||||
S32 space = buf_size - 1 - len;
|
||||
if (clip_len > space) clip_len = space;
|
||||
if (clip_len > 0) {
|
||||
memmove(&buf[g_wstate.cursor_pos + clip_len], &buf[g_wstate.cursor_pos], len - g_wstate.cursor_pos + 1);
|
||||
memcpy(&buf[g_wstate.cursor_pos], clip, clip_len);
|
||||
g_wstate.cursor_pos += clip_len;
|
||||
len += clip_len;
|
||||
text_changed = 1;
|
||||
}
|
||||
g_wstate.sel_start = g_wstate.cursor_pos;
|
||||
g_wstate.sel_end = g_wstate.cursor_pos;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Other Ctrl combos: ignore
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case PKEY_BACKSPACE:
|
||||
if (g_wstate.cursor_pos > 0) {
|
||||
if (text_input_has_sel()) {
|
||||
len = text_input_delete_sel(buf, len);
|
||||
text_changed = 1;
|
||||
} else 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;
|
||||
}
|
||||
g_wstate.sel_start = g_wstate.cursor_pos;
|
||||
g_wstate.sel_end = g_wstate.cursor_pos;
|
||||
break;
|
||||
case PKEY_DELETE:
|
||||
if (g_wstate.cursor_pos < len) {
|
||||
if (text_input_has_sel()) {
|
||||
len = text_input_delete_sel(buf, len);
|
||||
text_changed = 1;
|
||||
} else 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;
|
||||
}
|
||||
g_wstate.sel_start = g_wstate.cursor_pos;
|
||||
g_wstate.sel_end = g_wstate.cursor_pos;
|
||||
break;
|
||||
case PKEY_LEFT:
|
||||
if (g_wstate.cursor_pos > 0) g_wstate.cursor_pos--;
|
||||
if (text_input_has_sel()) {
|
||||
S32 lo, hi; text_input_get_sel(&lo, &hi);
|
||||
g_wstate.cursor_pos = lo;
|
||||
} else if (g_wstate.cursor_pos > 0) {
|
||||
g_wstate.cursor_pos--;
|
||||
}
|
||||
g_wstate.sel_start = g_wstate.cursor_pos;
|
||||
g_wstate.sel_end = g_wstate.cursor_pos;
|
||||
g_wstate.cursor_blink = 0;
|
||||
break;
|
||||
case PKEY_RIGHT:
|
||||
if (g_wstate.cursor_pos < len) g_wstate.cursor_pos++;
|
||||
if (text_input_has_sel()) {
|
||||
S32 lo, hi; text_input_get_sel(&lo, &hi);
|
||||
g_wstate.cursor_pos = hi;
|
||||
} else if (g_wstate.cursor_pos < len) {
|
||||
g_wstate.cursor_pos++;
|
||||
}
|
||||
g_wstate.sel_start = g_wstate.cursor_pos;
|
||||
g_wstate.sel_end = g_wstate.cursor_pos;
|
||||
g_wstate.cursor_blink = 0;
|
||||
break;
|
||||
case PKEY_HOME:
|
||||
g_wstate.cursor_pos = 0;
|
||||
g_wstate.sel_start = 0;
|
||||
g_wstate.sel_end = 0;
|
||||
g_wstate.cursor_blink = 0;
|
||||
break;
|
||||
case PKEY_END:
|
||||
g_wstate.cursor_pos = len;
|
||||
g_wstate.sel_start = len;
|
||||
g_wstate.sel_end = len;
|
||||
g_wstate.cursor_blink = 0;
|
||||
break;
|
||||
case PKEY_RETURN:
|
||||
@@ -310,46 +483,69 @@ B32 ui_text_input(const char *id, char *buf, S32 buf_size) {
|
||||
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;
|
||||
if (ch >= 32 && ch < 127) {
|
||||
// Delete selection first if any
|
||||
if (text_input_has_sel()) {
|
||||
len = text_input_delete_sel(buf, len);
|
||||
}
|
||||
if (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;
|
||||
}
|
||||
g_wstate.sel_start = g_wstate.cursor_pos;
|
||||
g_wstate.sel_end = g_wstate.cursor_pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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';
|
||||
// Tab cycling: find next text input after this one
|
||||
if (wants_tab && is_focused) {
|
||||
// We need to find our index. Since text inputs register in order,
|
||||
// our ID was just added above. Find it and advance to next.
|
||||
S32 my_idx = -1;
|
||||
for (S32 i = 0; i < g_wstate.text_input_count; i++) {
|
||||
if (g_wstate.text_input_ids[i] == eid.id) { my_idx = i; break; }
|
||||
}
|
||||
if (my_idx >= 0) {
|
||||
// Focus next (wrapping). But we might not have all inputs registered
|
||||
// yet this frame. Store the "next index" request; actual focus change
|
||||
// happens via a simple approach: just focus the next ID if known,
|
||||
// otherwise wrap to first.
|
||||
S32 next_idx = my_idx + 1;
|
||||
// We can't know total count yet (more inputs may register after us).
|
||||
// Instead, unfocus ourselves and set a "pending tab focus" that will
|
||||
// be resolved. Simpler approach: Tab from last input wraps to first,
|
||||
// Tab from others goes to next. Since we know the IDs registered so
|
||||
// far, and inputs are registered in declaration order:
|
||||
// If there IS a next already-registered input, go to it.
|
||||
// Otherwise, wrap to index 0.
|
||||
if (next_idx < g_wstate.text_input_count) {
|
||||
g_wstate.focused_id = g_wstate.text_input_ids[next_idx];
|
||||
} else {
|
||||
// Wrap to first
|
||||
g_wstate.focused_id = g_wstate.text_input_ids[0];
|
||||
}
|
||||
g_wstate.cursor_pos = 0;
|
||||
g_wstate.sel_start = 0;
|
||||
g_wstate.sel_end = 0;
|
||||
is_focused = 0; // no longer focused on THIS input
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
// Build display
|
||||
S32 len = (S32)strlen(buf);
|
||||
S32 sel_lo = 0, sel_hi = 0;
|
||||
B32 has_sel = 0;
|
||||
if (is_focused) {
|
||||
text_input_get_sel(&sel_lo, &sel_hi);
|
||||
if (sel_lo < 0) sel_lo = 0;
|
||||
if (sel_hi > len) sel_hi = len;
|
||||
has_sel = (sel_lo != sel_hi);
|
||||
}
|
||||
|
||||
Clay_Color bg = is_focused ? g_theme.bg_dark : g_theme.bg_medium;
|
||||
@@ -360,12 +556,68 @@ B32 ui_text_input(const char *id, char *buf, S32 buf_size) {
|
||||
.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 = border_color, .width = { 1, 1, 1, 1 } }
|
||||
) {
|
||||
CLAY_TEXT(clay_str(show_text), text_cfg);
|
||||
if (len == 0 && !is_focused) {
|
||||
// Placeholder
|
||||
CLAY_TEXT(CLAY_STRING("..."), &g_widget_text_config_dim);
|
||||
} else if (is_focused && has_sel) {
|
||||
// Three segments: before | selected | after
|
||||
// Before selection
|
||||
if (sel_lo > 0) {
|
||||
S32 n = sel_lo < (S32)DISPLAY_BUF_SIZE - 1 ? sel_lo : (S32)DISPLAY_BUF_SIZE - 1;
|
||||
memcpy(dbuf_before, buf, n);
|
||||
dbuf_before[n] = '\0';
|
||||
CLAY_TEXT(clay_str(dbuf_before), &g_widget_text_config);
|
||||
}
|
||||
// Selected text with highlight background
|
||||
{
|
||||
S32 slen = sel_hi - sel_lo;
|
||||
if (slen > (S32)DISPLAY_BUF_SIZE - 1) slen = (S32)DISPLAY_BUF_SIZE - 1;
|
||||
memcpy(dbuf_sel, &buf[sel_lo], slen);
|
||||
dbuf_sel[slen] = '\0';
|
||||
CLAY(WIDI(id, 900),
|
||||
.layout = {
|
||||
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
|
||||
},
|
||||
.backgroundColor = g_theme.accent
|
||||
) {
|
||||
CLAY_TEXT(clay_str(dbuf_sel), &g_widget_text_config_sel);
|
||||
}
|
||||
}
|
||||
// After selection
|
||||
if (sel_hi < len) {
|
||||
S32 n = len - sel_hi;
|
||||
if (n > (S32)DISPLAY_BUF_SIZE - 1) n = (S32)DISPLAY_BUF_SIZE - 1;
|
||||
memcpy(dbuf_after, &buf[sel_hi], n);
|
||||
dbuf_after[n] = '\0';
|
||||
CLAY_TEXT(clay_str(dbuf_after), &g_widget_text_config);
|
||||
}
|
||||
} else {
|
||||
// No selection: show text with cursor '|'
|
||||
if (is_focused) {
|
||||
S32 cp = g_wstate.cursor_pos;
|
||||
if (cp > len) cp = len;
|
||||
S32 total = len + 1; // +1 for cursor char
|
||||
if (total > (S32)DISPLAY_BUF_SIZE - 1) total = (S32)DISPLAY_BUF_SIZE - 1;
|
||||
S32 before = cp < total ? cp : total;
|
||||
memcpy(dbuf_before, buf, before);
|
||||
dbuf_before[before] = '|';
|
||||
S32 after = total - before - 1;
|
||||
if (after > 0) memcpy(dbuf_before + before + 1, buf + cp, after);
|
||||
dbuf_before[total] = '\0';
|
||||
CLAY_TEXT(clay_str(dbuf_before), &g_widget_text_config);
|
||||
} else {
|
||||
S32 n = len < (S32)DISPLAY_BUF_SIZE - 1 ? len : (S32)DISPLAY_BUF_SIZE - 1;
|
||||
memcpy(dbuf_before, buf, n);
|
||||
dbuf_before[n] = '\0';
|
||||
CLAY_TEXT(clay_str(dbuf_before), &g_widget_text_config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return text_changed;
|
||||
|
||||
Reference in New Issue
Block a user