update widgets, add copy paste buffer
This commit is contained in:
@@ -17,7 +17,7 @@ target:
|
|||||||
debug_info:
|
debug_info:
|
||||||
{
|
{
|
||||||
path: "C:/Users/mta/projects/autosample/build/autosample.pdb"
|
path: "C:/Users/mta/projects/autosample/build/autosample.pdb"
|
||||||
timestamp: 66207544041290
|
timestamp: 66207573824751
|
||||||
}
|
}
|
||||||
target:
|
target:
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -79,3 +79,9 @@ int32_t platform_poll_menu_command(PlatformWindow *window);
|
|||||||
|
|
||||||
// Returns accumulated input events since last call, then clears the buffer.
|
// Returns accumulated input events since last call, then clears the buffer.
|
||||||
PlatformInputEvents platform_get_input_events(PlatformWindow *window);
|
PlatformInputEvents platform_get_input_events(PlatformWindow *window);
|
||||||
|
|
||||||
|
// Clipboard operations (null-terminated UTF-8 strings).
|
||||||
|
// platform_clipboard_set copies text to the system clipboard.
|
||||||
|
// platform_clipboard_get returns a pointer to a static buffer (valid until next call), or nullptr.
|
||||||
|
void platform_clipboard_set(const char *text);
|
||||||
|
const char *platform_clipboard_get();
|
||||||
|
|||||||
@@ -206,3 +206,51 @@ PlatformInputEvents platform_get_input_events(PlatformWindow *window) {
|
|||||||
window->input_events = {};
|
window->input_events = {};
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void platform_clipboard_set(const char *text) {
|
||||||
|
if (!text) return;
|
||||||
|
int len = (int)strlen(text);
|
||||||
|
if (len == 0) return;
|
||||||
|
|
||||||
|
// Convert UTF-8 to wide string for Windows clipboard
|
||||||
|
int wlen = MultiByteToWideChar(CP_UTF8, 0, text, len, nullptr, 0);
|
||||||
|
if (wlen == 0) return;
|
||||||
|
|
||||||
|
HGLOBAL hmem = GlobalAlloc(GMEM_MOVEABLE, (wlen + 1) * sizeof(wchar_t));
|
||||||
|
if (!hmem) return;
|
||||||
|
|
||||||
|
wchar_t *wbuf = (wchar_t *)GlobalLock(hmem);
|
||||||
|
MultiByteToWideChar(CP_UTF8, 0, text, len, wbuf, wlen);
|
||||||
|
wbuf[wlen] = L'\0';
|
||||||
|
GlobalUnlock(hmem);
|
||||||
|
|
||||||
|
HWND hwnd = g_current_window ? g_current_window->hwnd : nullptr;
|
||||||
|
if (OpenClipboard(hwnd)) {
|
||||||
|
EmptyClipboard();
|
||||||
|
SetClipboardData(CF_UNICODETEXT, hmem);
|
||||||
|
CloseClipboard();
|
||||||
|
} else {
|
||||||
|
GlobalFree(hmem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *platform_clipboard_get() {
|
||||||
|
static char buf[4096];
|
||||||
|
buf[0] = '\0';
|
||||||
|
|
||||||
|
HWND hwnd = g_current_window ? g_current_window->hwnd : nullptr;
|
||||||
|
if (!OpenClipboard(hwnd)) return nullptr;
|
||||||
|
|
||||||
|
HGLOBAL hmem = GetClipboardData(CF_UNICODETEXT);
|
||||||
|
if (hmem) {
|
||||||
|
wchar_t *wbuf = (wchar_t *)GlobalLock(hmem);
|
||||||
|
if (wbuf) {
|
||||||
|
int len = WideCharToMultiByte(CP_UTF8, 0, wbuf, -1, buf, sizeof(buf) - 1, nullptr, nullptr);
|
||||||
|
buf[len > 0 ? len - 1 : 0] = '\0'; // WideCharToMultiByte includes null in count
|
||||||
|
GlobalUnlock(hmem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseClipboard();
|
||||||
|
return buf[0] ? buf : nullptr;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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_down = mouse_down;
|
||||||
g_wstate.mouse_clicked = (mouse_down && !g_wstate.was_mouse_down);
|
g_wstate.mouse_clicked = (mouse_down && !g_wstate.was_mouse_down);
|
||||||
g_wstate.cursor_blink += 1.0f / 60.0f;
|
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();
|
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;
|
||||||
static Clay_TextElementConfig g_widget_text_config_dim;
|
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 bool g_widget_text_configs_init = false;
|
||||||
|
|
||||||
static void ensure_widget_text_configs() {
|
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.textColor = g_theme.text_dim;
|
||||||
g_widget_text_config_dim.fontSize = 15;
|
g_widget_text_config_dim.fontSize = 15;
|
||||||
g_widget_text_config_dim.wrapMode = CLAY_TEXT_WRAP_NONE;
|
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) {
|
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
|
// 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
|
// remain valid until the end of the frame. We support up to
|
||||||
// MAX_TEXT_INPUTS simultaneous text inputs per frame.
|
// 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
|
#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;
|
static S32 g_text_display_buf_idx = 0;
|
||||||
|
|
||||||
void ui_text_input_reset_display_bufs() {
|
void ui_text_input_reset_display_bufs() {
|
||||||
g_text_display_buf_idx = 0;
|
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) {
|
B32 ui_text_input(const char *id, char *buf, S32 buf_size) {
|
||||||
ensure_widget_text_configs();
|
ensure_widget_text_configs();
|
||||||
B32 text_changed = 0;
|
B32 text_changed = 0;
|
||||||
|
|
||||||
// Grab a unique display buffer for this text input
|
// Register this text input for Tab cycling
|
||||||
S32 my_buf_idx = g_text_display_buf_idx++;
|
|
||||||
if (my_buf_idx >= MAX_TEXT_INPUTS) my_buf_idx = MAX_TEXT_INPUTS - 1;
|
|
||||||
char *display_buf = g_text_display_bufs[my_buf_idx];
|
|
||||||
|
|
||||||
Clay_ElementId eid = WID(id);
|
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 hovered = Clay_PointerOver(eid);
|
||||||
B32 is_focused = (g_wstate.focused_id == eid.id);
|
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
|
// Click to focus / unfocus
|
||||||
if (g_wstate.mouse_clicked) {
|
if (g_wstate.mouse_clicked) {
|
||||||
if (hovered) {
|
if (hovered) {
|
||||||
if (!is_focused) {
|
if (!is_focused) {
|
||||||
g_wstate.focused_id = eid.id;
|
g_wstate.focused_id = eid.id;
|
||||||
g_wstate.cursor_pos = (S32)strlen(buf);
|
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;
|
g_wstate.cursor_blink = 0;
|
||||||
is_focused = 1;
|
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) {
|
} else if (is_focused) {
|
||||||
g_wstate.focused_id = 0;
|
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
|
// Process keyboard input if focused
|
||||||
if (is_focused) {
|
if (is_focused) {
|
||||||
S32 len = (S32)strlen(buf);
|
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 > len) g_wstate.cursor_pos = len;
|
||||||
if (g_wstate.cursor_pos < 0) g_wstate.cursor_pos = 0;
|
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;
|
// Key events
|
||||||
// we handle backspace/delete/arrows via keys, printable chars via chars)
|
|
||||||
for (S32 k = 0; k < g_wstate.input.key_count; k++) {
|
for (S32 k = 0; k < g_wstate.input.key_count; k++) {
|
||||||
uint8_t key = g_wstate.input.keys[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) {
|
switch (key) {
|
||||||
case PKEY_BACKSPACE:
|
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);
|
memmove(&buf[g_wstate.cursor_pos - 1], &buf[g_wstate.cursor_pos], len - g_wstate.cursor_pos + 1);
|
||||||
g_wstate.cursor_pos--;
|
g_wstate.cursor_pos--;
|
||||||
len--;
|
len--;
|
||||||
text_changed = 1;
|
text_changed = 1;
|
||||||
}
|
}
|
||||||
|
g_wstate.sel_start = g_wstate.cursor_pos;
|
||||||
|
g_wstate.sel_end = g_wstate.cursor_pos;
|
||||||
break;
|
break;
|
||||||
case PKEY_DELETE:
|
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);
|
memmove(&buf[g_wstate.cursor_pos], &buf[g_wstate.cursor_pos + 1], len - g_wstate.cursor_pos);
|
||||||
len--;
|
len--;
|
||||||
text_changed = 1;
|
text_changed = 1;
|
||||||
}
|
}
|
||||||
|
g_wstate.sel_start = g_wstate.cursor_pos;
|
||||||
|
g_wstate.sel_end = g_wstate.cursor_pos;
|
||||||
break;
|
break;
|
||||||
case PKEY_LEFT:
|
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;
|
g_wstate.cursor_blink = 0;
|
||||||
break;
|
break;
|
||||||
case PKEY_RIGHT:
|
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;
|
g_wstate.cursor_blink = 0;
|
||||||
break;
|
break;
|
||||||
case PKEY_HOME:
|
case PKEY_HOME:
|
||||||
g_wstate.cursor_pos = 0;
|
g_wstate.cursor_pos = 0;
|
||||||
|
g_wstate.sel_start = 0;
|
||||||
|
g_wstate.sel_end = 0;
|
||||||
g_wstate.cursor_blink = 0;
|
g_wstate.cursor_blink = 0;
|
||||||
break;
|
break;
|
||||||
case PKEY_END:
|
case PKEY_END:
|
||||||
g_wstate.cursor_pos = len;
|
g_wstate.cursor_pos = len;
|
||||||
|
g_wstate.sel_start = len;
|
||||||
|
g_wstate.sel_end = len;
|
||||||
g_wstate.cursor_blink = 0;
|
g_wstate.cursor_blink = 0;
|
||||||
break;
|
break;
|
||||||
case PKEY_RETURN:
|
case PKEY_RETURN:
|
||||||
@@ -310,46 +483,69 @@ B32 ui_text_input(const char *id, char *buf, S32 buf_size) {
|
|||||||
if (is_focused) {
|
if (is_focused) {
|
||||||
for (S32 c = 0; c < g_wstate.input.char_count; c++) {
|
for (S32 c = 0; c < g_wstate.input.char_count; c++) {
|
||||||
uint16_t ch = g_wstate.input.chars[c];
|
uint16_t ch = g_wstate.input.chars[c];
|
||||||
if (ch >= 32 && ch < 127 && len < buf_size - 1) {
|
if (ch >= 32 && ch < 127) {
|
||||||
memmove(&buf[g_wstate.cursor_pos + 1], &buf[g_wstate.cursor_pos], len - g_wstate.cursor_pos + 1);
|
// Delete selection first if any
|
||||||
buf[g_wstate.cursor_pos] = (char)ch;
|
if (text_input_has_sel()) {
|
||||||
g_wstate.cursor_pos++;
|
len = text_input_delete_sel(buf, len);
|
||||||
len++;
|
}
|
||||||
text_changed = 1;
|
if (len < buf_size - 1) {
|
||||||
g_wstate.cursor_blink = 0;
|
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.
|
// Tab cycling: find next text input after this one
|
||||||
// The cursor is shown as a '|' character when focused. To avoid layout
|
if (wants_tab && is_focused) {
|
||||||
// flicker from the cursor blinking (which changes text width), we always
|
// We need to find our index. Since text inputs register in order,
|
||||||
// include the cursor character when focused and simply don't blink it.
|
// our ID was just added above. Find it and advance to next.
|
||||||
S32 len = (S32)strlen(buf);
|
S32 my_idx = -1;
|
||||||
|
for (S32 i = 0; i < g_wstate.text_input_count; i++) {
|
||||||
if (is_focused) {
|
if (g_wstate.text_input_ids[i] == eid.id) { my_idx = i; break; }
|
||||||
S32 cp = g_wstate.cursor_pos;
|
}
|
||||||
if (cp > len) cp = len;
|
if (my_idx >= 0) {
|
||||||
S32 total = len + 1; // +1 for cursor char
|
// Focus next (wrapping). But we might not have all inputs registered
|
||||||
if (total > 510) total = 510;
|
// yet this frame. Store the "next index" request; actual focus change
|
||||||
S32 before = cp < total ? cp : total;
|
// happens via a simple approach: just focus the next ID if known,
|
||||||
memcpy(display_buf, buf, before);
|
// otherwise wrap to first.
|
||||||
display_buf[before] = '|';
|
S32 next_idx = my_idx + 1;
|
||||||
S32 after = total - before - 1;
|
// We can't know total count yet (more inputs may register after us).
|
||||||
if (after > 0) memcpy(display_buf + before + 1, buf + cp, after);
|
// Instead, unfocus ourselves and set a "pending tab focus" that will
|
||||||
display_buf[total] = '\0';
|
// be resolved. Simpler approach: Tab from last input wraps to first,
|
||||||
} else {
|
// Tab from others goes to next. Since we know the IDs registered so
|
||||||
S32 copy_len = len < 511 ? len : 511;
|
// far, and inputs are registered in declaration order:
|
||||||
memcpy(display_buf, buf, copy_len);
|
// If there IS a next already-registered input, go to it.
|
||||||
display_buf[copy_len] = '\0';
|
// 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;
|
// Build display
|
||||||
Clay_TextElementConfig *text_cfg = &g_widget_text_config;
|
S32 len = (S32)strlen(buf);
|
||||||
if (len == 0 && !is_focused) {
|
S32 sel_lo = 0, sel_hi = 0;
|
||||||
show_text = "...";
|
B32 has_sel = 0;
|
||||||
text_cfg = &g_widget_text_config_dim;
|
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;
|
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) },
|
.sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(30) },
|
||||||
.padding = { 8, 8, 0, 0 },
|
.padding = { 8, 8, 0, 0 },
|
||||||
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
|
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
|
||||||
|
.layoutDirection = CLAY_LEFT_TO_RIGHT,
|
||||||
},
|
},
|
||||||
.backgroundColor = bg,
|
.backgroundColor = bg,
|
||||||
.cornerRadius = CLAY_CORNER_RADIUS(3),
|
.cornerRadius = CLAY_CORNER_RADIUS(3),
|
||||||
.border = { .color = border_color, .width = { 1, 1, 1, 1 } }
|
.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;
|
return text_changed;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
// Widget state (global, managed by widget layer)
|
// Widget state (global, managed by widget layer)
|
||||||
|
|
||||||
#define UI_WIDGET_MAX_DROPDOWN_ITEMS 32
|
#define UI_WIDGET_MAX_DROPDOWN_ITEMS 32
|
||||||
|
#define UI_WIDGET_MAX_TEXT_INPUTS 16
|
||||||
|
|
||||||
struct UI_WidgetState {
|
struct UI_WidgetState {
|
||||||
// Text input focus
|
// Text input focus
|
||||||
@@ -20,6 +21,15 @@ struct UI_WidgetState {
|
|||||||
int32_t cursor_pos; // Cursor position in focused text input
|
int32_t cursor_pos; // Cursor position in focused text input
|
||||||
F32 cursor_blink; // Blink timer (seconds)
|
F32 cursor_blink; // Blink timer (seconds)
|
||||||
|
|
||||||
|
// Text selection (sel_start == sel_end means no selection)
|
||||||
|
int32_t sel_start; // Selection anchor (where selection began)
|
||||||
|
int32_t sel_end; // Selection extent (moves with cursor)
|
||||||
|
|
||||||
|
// Tab cycling: registered text input IDs in order of declaration
|
||||||
|
uint32_t text_input_ids[UI_WIDGET_MAX_TEXT_INPUTS];
|
||||||
|
int32_t text_input_count;
|
||||||
|
B32 tab_pressed; // True on the frame Tab was pressed
|
||||||
|
|
||||||
// Dropdown
|
// Dropdown
|
||||||
uint32_t open_dropdown_id; // Clay element ID hash of the open dropdown (0 = none)
|
uint32_t open_dropdown_id; // Clay element ID hash of the open dropdown (0 = none)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user