feat: switch text to UTF-8; add Unicode key events, dead keys & Czech layout; fix numpad navigation; add NumLock support

This commit is contained in:
daniel committed 2026-10-03 10:11:49 +02:00
1 parent 32660ddd1a
commit 2aa6360b3d
101 files changed
+3733 -1639

No files matched your search

+66 -38
View File
@@ -296,15 +296,13 @@ inline bool text_input_has_selection(const TextInputState& state, const char* te
return text_input_selection_start(state, len) != text_input_selection_end(state, len);
}
// Width of the first `count` bytes (a character boundary). Masked fields
// draw one '*' per character, however many bytes it takes.
inline int text_input_prefix_width(const char* text, int count, bool masked) {
char prefix[512];
int len = 0;
while (text && text[len] && len < count && len < (int)sizeof(prefix) - 1) {
prefix[len] = masked ? '*' : text[len];
len++;
}
prefix[len] = '\0';
return text_width(prefix);
while (text && text[len] && len < count) len++;
if (!masked) return text_width_n(text, len);
return montauk::utf8::count(text, len) * text_width("*");
}
inline int text_input_index_from_x(const Rect& bounds, const char* value,
@@ -314,11 +312,13 @@ inline int text_input_index_from_x(const Rect& bounds, const char* value,
int len = text_input_len(value);
int prev_w = 0;
for (int i = 0; i < len; i++) {
int cur_w = text_input_prefix_width(value, i + 1, masked);
for (int i = 0; i < len;) {
int next = montauk::utf8::next_boundary(value, len, i);
int cur_w = text_input_prefix_width(value, next, masked);
int midpoint = prev_w + (cur_w - prev_w) / 2;
if (rel_x < midpoint) return i;
prev_w = cur_w;
i = next;
}
return len;
}
@@ -360,8 +360,11 @@ inline int text_input_mouse(TextInputState& state,
return TEXT_INPUT_NONE;
}
// Bytes >= 0x80 (any non-ASCII character) count as word characters, so word
// jumps never stop inside a multi-byte sequence.
inline bool text_input_word_char(char ch) {
return (ch >= 'a' && ch <= 'z') ||
return (uint8_t)ch >= 0x80 ||
(ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '_';
@@ -574,8 +577,10 @@ inline int text_input_key(TextInputState& state,
if (!key.pressed || !text || cap <= 0) return TEXT_INPUT_NONE;
int len = text_input_len(text, cap);
state.cursor = text_input_clamp_cursor(state.cursor, len);
state.selection_anchor = text_input_clamp_cursor(state.selection_anchor, len);
state.cursor = montauk::utf8::floor_boundary(
text, len, text_input_clamp_cursor(state.cursor, len));
state.selection_anchor = montauk::utf8::floor_boundary(
text, len, text_input_clamp_cursor(state.selection_anchor, len));
if (state.context.open) {
context_menu_close(state.context);
@@ -606,20 +611,22 @@ inline int text_input_key(TextInputState& state,
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
}
if (key.scancode == 0x4B || key.scancode == 0x4D ||
key.scancode == 0x47 || key.scancode == 0x4F) {
if (montauk::nav_scancode(key) == 0x4B || montauk::nav_scancode(key) == 0x4D ||
montauk::nav_scancode(key) == 0x47 || montauk::nav_scancode(key) == 0x4F) {
int next = state.cursor;
if (!key.shift && sel_start != sel_end && key.scancode == 0x4B)
if (!key.shift && sel_start != sel_end && montauk::nav_scancode(key) == 0x4B)
next = sel_start;
else if (!key.shift && sel_start != sel_end && key.scancode == 0x4D)
else if (!key.shift && sel_start != sel_end && montauk::nav_scancode(key) == 0x4D)
next = sel_end;
else if (key.scancode == 0x4B)
next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor) : state.cursor - 1;
else if (key.scancode == 0x4D)
next = key.ctrl ? text_input_next_word_boundary(text, state.cursor) : state.cursor + 1;
else if (key.scancode == 0x47)
else if (montauk::nav_scancode(key) == 0x4B)
next = key.ctrl ? text_input_prev_word_boundary(text, state.cursor)
: montauk::utf8::prev_boundary(text, state.cursor);
else if (montauk::nav_scancode(key) == 0x4D)
next = key.ctrl ? text_input_next_word_boundary(text, state.cursor)
: montauk::utf8::next_boundary(text, len, state.cursor);
else if (montauk::nav_scancode(key) == 0x47)
next = 0;
else if (key.scancode == 0x4F)
else if (montauk::nav_scancode(key) == 0x4F)
next = len;
text_input_set_cursor(state, len, next, key.shift);
@@ -641,15 +648,16 @@ inline int text_input_key(TextInputState& state,
changed = true;
}
} else if (state.cursor > 0) {
text_input_delete_range(text, &len, state.cursor - 1, state.cursor);
state.cursor--;
int start = montauk::utf8::prev_boundary(text, state.cursor);
text_input_delete_range(text, &len, start, state.cursor);
state.cursor = start;
changed = true;
}
state.selection_anchor = state.cursor;
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
}
if (key.scancode == 0x53) {
if (montauk::nav_scancode(key) == 0x53) {
if (sel_start != sel_end) {
text_input_delete_range(text, &len, sel_start, sel_end);
state.cursor = sel_start;
@@ -661,15 +669,22 @@ inline int text_input_key(TextInputState& state,
changed = true;
}
} else if (state.cursor < len) {
text_input_delete_range(text, &len, state.cursor, state.cursor + 1);
int end = montauk::utf8::next_boundary(text, len, state.cursor);
text_input_delete_range(text, &len, state.cursor, end);
changed = true;
}
state.selection_anchor = state.cursor;
return TEXT_INPUT_CONSUMED | (changed ? TEXT_INPUT_CHANGED : 0);
}
if (key.ctrl || key.alt || !text_input_char_allowed(key.ascii, filter, userdata))
return TEXT_INPUT_NONE;
// Typed text is inserted as UTF-8; the filter sees each encoded byte.
char typed[montauk::utf8::MAX_BYTES];
int typed_len = key.ctrl || key.alt ? 0 : montauk::utf8::key_utf8(key, typed);
if (typed_len == 0) return TEXT_INPUT_NONE;
for (int i = 0; i < typed_len; i++) {
if (!text_input_char_allowed(typed[i], filter, userdata))
return TEXT_INPUT_NONE;
}
if (sel_start != sel_end) {
text_input_delete_range(text, &len, sel_start, sel_end);
@@ -677,11 +692,12 @@ inline int text_input_key(TextInputState& state,
changed = true;
}
if (len < cap - 1) {
if (len + typed_len < cap) {
for (int i = len; i >= state.cursor; i--)
text[i + 1] = text[i];
text[state.cursor++] = key.ascii;
len++;
text[i + typed_len] = text[i];
for (int i = 0; i < typed_len; i++)
text[state.cursor++] = typed[i];
len += typed_len;
changed = true;
}
@@ -1251,22 +1267,34 @@ inline void draw_badge(Canvas& c,
c.text(x + theme.gap_sm, y + 2, label, fg);
}
// One '*' per UTF-8 character.
inline const char* mask_text(const char* value, char* buffer, int cap) {
int len = 0;
while (value && value[len] && len < cap - 1) {
buffer[len] = '*';
len++;
const char* p = value ? value : "";
while (*p && len < cap - 1) {
montauk::utf8::next(&p);
buffer[len++] = '*';
}
buffer[len] = '\0';
return buffer;
}
// First `count` bytes of value (a character boundary), or one '*' per
// character in them when masked.
inline void copy_prefix(char* buffer, int cap, const char* value, int count, bool masked) {
if (cap <= 0) return;
int len = 0;
while (value && value[len] && len < count && len < cap - 1) {
buffer[len] = masked ? '*' : value[len];
len++;
if (masked) {
const char* p = value ? value : "";
while (*p && p - value < count && len < cap - 1) {
montauk::utf8::next(&p);
buffer[len++] = '*';
}
} else {
while (value && value[len] && len < count && len < cap - 1) {
buffer[len] = value[len];
len++;
}
}
buffer[len] = '\0';
}