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

+2 -2
View File
@@ -165,9 +165,9 @@ struct Canvas {
if (!str || !font_data || scale <= 0) return;
uint32_t pixel = c.to_pixel();
int cx = x;
for (int i = 0; str[i]; i++, cx += FONT_WIDTH * scale) {
for (const char* p = str; *p; cx += FONT_WIDTH * scale) {
const uint8_t* glyph =
&font_data[(unsigned char)str[i] * FONT_HEIGHT];
&font_data[cp437_index(montauk::utf8::next(&p)) * FONT_HEIGHT];
for (int row = 0; row < FONT_HEIGHT; row++) {
uint8_t bits = glyph[row];
if (!bits) continue;
+28
View File
@@ -9,12 +9,32 @@
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/heap.h>
#include <montauk/utf8.h>
namespace gui {
// The clipboard carries UTF-8. Text that is not well-formed (legacy
// Windows-1252 bytes) is converted on the way in and on the way out, so
// every reader can assume valid UTF-8.
inline char* clipboard_utf8_copy(const char* text, int len, int* out_len) {
int cap = len * 3 + 1; // each stray byte grows to at most 3
char* out = (char*)montauk::malloc(cap);
if (!out) return nullptr;
*out_len = montauk::utf8::normalize(text, len, out, cap);
return out;
}
inline bool clipboard_set_text(const char* text, int len) {
if (len < 0) return false;
if (len > 0 && text == nullptr) return false;
if (len > 0 && !montauk::utf8::valid(text, len)) {
int fixed_len = 0;
char* fixed = clipboard_utf8_copy(text, len, &fixed_len);
if (!fixed) return false;
bool ok = montauk::clipboard_set_text(fixed, (uint32_t)fixed_len) == 0;
montauk::mfree(fixed);
return ok;
}
return montauk::clipboard_set_text(text, (uint32_t)len) == 0;
}
@@ -53,6 +73,14 @@ namespace gui {
}
text[actual_len] = '\0';
if (!montauk::utf8::valid(text, (int)actual_len)) {
int fixed_len = 0;
char* fixed = clipboard_utf8_copy(text, (int)actual_len, &fixed_len);
montauk::mfree(text);
if (!fixed) return nullptr;
text = fixed;
actual_len = (uint32_t)fixed_len;
}
if (out_len) *out_len = (int)actual_len;
if (out_serial) *out_serial = serial;
return text;
+40 -7
View File
@@ -19,6 +19,34 @@ static constexpr int FONT_HEIGHT = 16;
// TrueType fonts, while login/desktop can recover when TrueType setup fails.
extern const uint8_t font_data[256 * 16] __attribute__((weak));
// The bitmap fallback font is Code Page 437. Map a Unicode codepoint to its
// CP437 cell ('?' when it has none) so UTF-8 text still draws sensibly,
// including the box-drawing characters terminal programs use.
inline uint8_t cp437_index(uint32_t cp) {
if (cp < 0x80) return (uint8_t)cp;
static constexpr uint16_t high[128] = {
0x00C7,0x00FC,0x00E9,0x00E2,0x00E4,0x00E0,0x00E5,0x00E7,
0x00EA,0x00EB,0x00E8,0x00EF,0x00EE,0x00EC,0x00C4,0x00C5,
0x00C9,0x00E6,0x00C6,0x00F4,0x00F6,0x00F2,0x00FB,0x00F9,
0x00FF,0x00D6,0x00DC,0x00A2,0x00A3,0x00A5,0x20A7,0x0192,
0x00E1,0x00ED,0x00F3,0x00FA,0x00F1,0x00D1,0x00AA,0x00BA,
0x00BF,0x2310,0x00AC,0x00BD,0x00BC,0x00A1,0x00AB,0x00BB,
0x2591,0x2592,0x2593,0x2502,0x2524,0x2561,0x2562,0x2556,
0x2555,0x2563,0x2551,0x2557,0x255D,0x255C,0x255B,0x2510,
0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x255E,0x255F,
0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x2567,
0x2568,0x2564,0x2565,0x2559,0x2558,0x2552,0x2553,0x256B,
0x256A,0x2518,0x250C,0x2588,0x2584,0x258C,0x2590,0x2580,
0x03B1,0x00DF,0x0393,0x03C0,0x03A3,0x03C3,0x00B5,0x03C4,
0x03A6,0x0398,0x03A9,0x03B4,0x221E,0x03C6,0x03B5,0x2229,
0x2261,0x00B1,0x2265,0x2264,0x2320,0x2321,0x00F7,0x2248,
0x00B0,0x2219,0x00B7,0x221A,0x207F,0x00B2,0x25A0,0x00A0,
};
for (int i = 0; i < 128; i++)
if (high[i] == cp) return (uint8_t)(0x80 + i);
return '?';
}
// Dynamic font height: TTF line height or 16 (bitmap fallback)
inline int system_font_height() {
if (fonts::system_font && fonts::system_font->valid)
@@ -74,8 +102,8 @@ inline void draw_text(Framebuffer& fb, int x, int y, const char* text, Color fg)
fonts::system_font->draw(fb, x, y, text, fg, fonts::UI_SIZE);
return;
}
for (int i = 0; text[i]; i++) {
draw_char(fb, x + i * FONT_WIDTH, y, text[i], fg);
for (const char* p = text; *p; x += FONT_WIDTH) {
draw_char(fb, x, y, (char)cp437_index(montauk::utf8::next(&p)), fg);
}
}
@@ -84,18 +112,23 @@ inline void draw_text_bg(Framebuffer& fb, int x, int y, const char* text, Color
fonts::system_font->draw_bg(fb, x, y, text, fg, bg, fonts::UI_SIZE);
return;
}
for (int i = 0; text[i]; i++) {
draw_char_bg(fb, x + i * FONT_WIDTH, y, text[i], fg, bg);
for (const char* p = text; *p; x += FONT_WIDTH) {
draw_char_bg(fb, x, y, (char)cp437_index(montauk::utf8::next(&p)), fg, bg);
}
}
// Width of the first `len` bytes of a UTF-8 string in the UI font.
inline int text_width_n(const char* text, int len) {
if (fonts::system_font && fonts::system_font->valid)
return fonts::system_font->measure_text_n(text, len, fonts::UI_SIZE);
return montauk::utf8::count(text, len) * FONT_WIDTH;
}
inline int text_width(const char* text) {
if (fonts::system_font && fonts::system_font->valid) {
return fonts::system_font->measure_text(text, fonts::UI_SIZE);
}
int len = 0;
while (text[len]) len++;
return len * FONT_WIDTH;
return montauk::utf8::count(text) * FONT_WIDTH;
}
} // namespace gui
+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';
}
+69 -13
View File
@@ -9,13 +9,14 @@
#include "gui/font.hpp"
#include <montauk/syscall.h>
#include <montauk/string.h>
#include <montauk/utf8.h>
#include <Api/Syscall.hpp>
#include <montauk/capabilities.h>
namespace gui {
struct TermCell {
char ch;
uint32_t ch; // Unicode codepoint (every cell is one column wide)
Color fg;
Color bg;
};
@@ -58,6 +59,11 @@ struct TerminalState {
int csi_params[8];
int csi_param_count;
int csi_current_param;
// Partial UTF-8 sequence carried across terminal_feed calls (child output
// arrives in arbitrary chunks, so a character can straddle two reads).
char utf8_pending[4];
int utf8_pending_len;
};
// ==== Palette ====
@@ -244,6 +250,7 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int
t->csi_private = false;
t->csi_param_count = 0;
t->csi_current_param = 0;
t->utf8_pending_len = 0;
t->child_pid = 0;
int total_cells = (rows + max_sb) * cols;
@@ -275,7 +282,7 @@ static inline void terminal_init(TerminalState* t, int cols, int rows) {
montauk::childio_settermsz(t->child_pid, cols, rows);
}
static inline void terminal_put_char(TerminalState* t, char ch) {
static inline void terminal_put_char(TerminalState* t, uint32_t ch) {
if (t->cursor_x >= t->cols) {
t->cursor_x = 0;
t->cursor_y++;
@@ -549,11 +556,61 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
}
}
// Emit pending bytes that did not form a valid UTF-8 sequence: each one is a
// Windows-1252 character, matching how the GUI renders legacy text.
static inline void terminal_flush_utf8(TerminalState* t) {
for (int i = 0; i < t->utf8_pending_len; i++)
terminal_put_char(t, montauk::utf8::from_cp1252((uint8_t)t->utf8_pending[i]));
t->utf8_pending_len = 0;
}
static inline int terminal_utf8_expected(uint8_t lead) {
if (lead >= 0xC2 && lead <= 0xDF) return 2;
if (lead >= 0xE0 && lead <= 0xEF) return 3;
if (lead >= 0xF0 && lead <= 0xF4) return 4;
return 0;
}
// Feed one byte >= 0x80 of printable text through the UTF-8 assembler.
static inline void terminal_feed_high(TerminalState* t, uint8_t b) {
if (t->utf8_pending_len > 0) {
if ((b & 0xC0) == 0x80) {
t->utf8_pending[t->utf8_pending_len++] = (char)b;
int want = terminal_utf8_expected((uint8_t)t->utf8_pending[0]);
uint32_t cp = 0;
int n = montauk::utf8::sequence_length(t->utf8_pending,
t->utf8_pending_len, &cp);
if (n == want) {
terminal_put_char(t, cp);
t->utf8_pending_len = 0;
} else if (t->utf8_pending_len >= want) {
terminal_flush_utf8(t); // overlong / surrogate / out of range
}
return;
}
terminal_flush_utf8(t);
}
if (terminal_utf8_expected(b)) {
t->utf8_pending[0] = (char)b;
t->utf8_pending_len = 1;
} else {
terminal_put_char(t, montauk::utf8::from_cp1252(b));
}
}
static inline void terminal_feed(TerminalState* t, const char* data, int len) {
if (len > 0) t->dirty = true;
for (int i = 0; i < len; i++) {
char ch = data[i];
if (t->parse_state == TerminalState::STATE_NORMAL) {
if ((uint8_t)ch >= 0x80) {
terminal_feed_high(t, (uint8_t)ch);
continue;
}
if (t->utf8_pending_len > 0) terminal_flush_utf8(t);
}
switch (t->parse_state) {
case TerminalState::STATE_NORMAL:
if (ch == '\033') {
@@ -575,9 +632,8 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) {
while (t->cursor_x < next) {
terminal_put_char(t, ' ');
}
} else if (ch >= 32 || ch < 0) {
// Printable character (also treat high-bit chars as printable)
terminal_put_char(t, ch);
} else if (ch >= 32) {
terminal_put_char(t, (uint8_t)ch);
}
break;
@@ -641,9 +697,9 @@ static inline void terminal_fill_pixel_rect(uint32_t* pixels, int pw, int ph,
}
static inline void terminal_draw_bitmap_char(uint32_t* pixels, int pw, int ph,
int x, int y, unsigned char ch,
int x, int y, uint32_t ch,
uint32_t color) {
const uint8_t* glyph = &font_data[ch * FONT_HEIGHT];
const uint8_t* glyph = &font_data[cp437_index(ch) * FONT_HEIGHT];
for (int fy = 0; fy < FONT_HEIGHT; fy++) {
int dy = y + fy;
if (dy >= ph) break;
@@ -669,14 +725,14 @@ static inline void terminal_draw_cell_pixels(const TermCell& cell,
cell.bg.to_pixel());
}
if (cell.ch <= 32 && cell.ch >= 0) return;
if (cell.ch <= 32) return;
if (use_ttf) {
int baseline = py + gc->ascent;
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
px, baseline, (unsigned char)cell.ch, cell.fg, gc);
px, baseline, (int)cell.ch, cell.fg, gc);
} else {
terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch,
terminal_draw_bitmap_char(pixels, pw, ph, px, py, cell.ch,
cell.fg.to_pixel());
}
}
@@ -691,14 +747,14 @@ static inline void terminal_draw_cursor_pixels(const TermCell& cell,
terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h,
g_term_palette.cursor.to_pixel());
if (cell.ch <= 32 && cell.ch >= 0) return;
if (cell.ch <= 32) return;
if (use_ttf) {
int baseline = py + gc->ascent;
fonts::mono->draw_char_to_buffer(pixels, pw, ph,
px, baseline, (unsigned char)cell.ch, g_term_palette.bg, gc);
px, baseline, (int)cell.ch, g_term_palette.bg, gc);
} else {
terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch,
terminal_draw_bitmap_char(pixels, pw, ph, px, py, cell.ch,
g_term_palette.bg.to_pixel());
}
}
+113 -51
View File
@@ -9,6 +9,7 @@
#include <montauk/syscall.h>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <montauk/utf8.h>
#include "gui/gui.hpp"
#include "gui/framebuffer.hpp"
@@ -41,39 +42,12 @@
namespace gui {
// Windows-1252 byte -> Unicode codepoint, for the remaining code that still
// handles single-byte text (PDF simple-font encodings). UTF-8 strings go
// through measure_text/draw, which decode with montauk::utf8.
inline int decode_single_byte_codepoint(int codepoint) {
if (codepoint < 0 || codepoint > 255) return codepoint;
switch (codepoint) {
case 0x80: return 0x20AC; // Euro Sign
case 0x82: return 0x201A; // Single Low-9 Quotation Mark
case 0x83: return 0x0192; // Latin Small Letter F With Hook
case 0x84: return 0x201E; // Double Low-9 Quotation Mark
case 0x85: return 0x2026; // Horizontal Ellipsis
case 0x86: return 0x2020; // Dagger
case 0x87: return 0x2021; // Double Dagger
case 0x88: return 0x02C6; // Modifier Letter Circumflex Accent
case 0x89: return 0x2030; // Per Mille Sign
case 0x8A: return 0x0160; // Latin Capital Letter S With Caron
case 0x8B: return 0x2039; // Single Left-Pointing Angle Quotation Mark
case 0x8C: return 0x0152; // Latin Capital Ligature OE
case 0x8E: return 0x017D; // Latin Capital Letter Z With Caron
case 0x91: return 0x2018; // Left Single Quotation Mark
case 0x92: return 0x2019; // Right Single Quotation Mark
case 0x93: return 0x201C; // Left Double Quotation Mark
case 0x94: return 0x201D; // Right Double Quotation Mark
case 0x95: return 0x2022; // Bullet
case 0x96: return 0x2013; // En Dash
case 0x97: return 0x2014; // Em Dash
case 0x98: return 0x02DC; // Small Tilde
case 0x99: return 0x2122; // Trade Mark Sign
case 0x9A: return 0x0161; // Latin Small Letter S With Caron
case 0x9B: return 0x203A; // Single Right-Pointing Angle Quotation Mark
case 0x9C: return 0x0153; // Latin Small Ligature OE
case 0x9E: return 0x017E; // Latin Small Letter Z With Caron
case 0x9F: return 0x0178; // Latin Capital Letter Y With Diaeresis
default: return codepoint;
}
return (int)montauk::utf8::from_cp1252((uint8_t)codepoint);
}
struct CachedGlyph {
@@ -84,8 +58,18 @@ struct CachedGlyph {
bool loaded;
};
// Glyphs above U+00FF live in a small open-addressed table so CJK, symbols
// and the like cost nothing until they are drawn.
struct ExtGlyph {
uint32_t codepoint; // 0 = empty slot
CachedGlyph glyph;
};
struct GlyphCache {
CachedGlyph glyphs[256]; // Single-byte text with Windows-1252 punctuation mapping
CachedGlyph glyphs[256]; // Unicode U+0000-U+00FF, indexed directly
ExtGlyph* ext; // everything above, hashed by codepoint
int ext_cap; // power of two, 0 until first use
int ext_count;
int pixel_size;
float scale;
int ascent, descent, line_gap;
@@ -142,8 +126,21 @@ struct TrueTypeFont {
return true;
}
void init_cache(GlyphCache* gc, int pixel_size) {
// Free any existing glyph bitmaps
static void free_ext(GlyphCache* gc) {
if (gc->ext) {
for (int i = 0; i < gc->ext_cap; i++) {
if (gc->ext[i].codepoint && gc->ext[i].glyph.bitmap)
montauk::mfree(gc->ext[i].glyph.bitmap);
}
montauk::mfree(gc->ext);
}
gc->ext = nullptr;
gc->ext_cap = 0;
gc->ext_count = 0;
}
// Release every glyph bitmap in a cache (the cache stays usable).
static void free_cache(GlyphCache* gc) {
for (int i = 0; i < 256; i++) {
if (gc->glyphs[i].bitmap) {
montauk::mfree(gc->glyphs[i].bitmap);
@@ -151,6 +148,11 @@ struct TrueTypeFont {
gc->glyphs[i].bitmap = nullptr;
gc->glyphs[i].loaded = false;
}
free_ext(gc);
}
void init_cache(GlyphCache* gc, int pixel_size) {
free_cache(gc);
gc->pixel_size = pixel_size;
gc->scale = em_scaling
@@ -179,11 +181,8 @@ struct TrueTypeFont {
return gc;
}
// Evict oldest cache (slot 0) — free its glyph bitmaps
for (int g = 0; g < 256; g++) {
if (caches[0].glyphs[g].bitmap)
montauk::mfree(caches[0].glyphs[g].bitmap);
}
// Evict oldest cache (slot 0) - free its glyph bitmaps
free_cache(&caches[0]);
// Shift remaining caches down
for (int i = 0; i < 3; i++)
montauk::memcpy(&caches[i], &caches[i + 1], sizeof(GlyphCache));
@@ -192,13 +191,54 @@ struct TrueTypeFont {
return &caches[3];
}
static constexpr int EXT_MAX = 2048; // flush the table beyond this
CachedGlyph* ext_slot(GlyphCache* gc, uint32_t codepoint) {
if (gc->ext_cap == 0 || gc->ext_count * 4 >= gc->ext_cap * 3) {
int new_cap = gc->ext_cap ? gc->ext_cap * 2 : 64;
if (gc->ext_count >= EXT_MAX) {
// A document full of distinct glyphs: start over rather than
// grow without bound. Bitmaps are rebuilt on demand.
free_ext(gc);
new_cap = 64;
}
ExtGlyph* table = (ExtGlyph*)montauk::malloc(sizeof(ExtGlyph) * new_cap);
if (!table) return nullptr;
montauk::memset(table, 0, sizeof(ExtGlyph) * new_cap);
for (int i = 0; i < gc->ext_cap; i++) {
ExtGlyph& e = gc->ext[i];
if (!e.codepoint) continue;
uint32_t h = (e.codepoint * 2654435761u) & (new_cap - 1);
while (table[h].codepoint) h = (h + 1) & (new_cap - 1);
table[h] = e;
}
if (gc->ext) montauk::mfree(gc->ext);
gc->ext = table;
gc->ext_cap = new_cap;
}
uint32_t mask = gc->ext_cap - 1;
uint32_t h = (codepoint * 2654435761u) & mask;
while (gc->ext[h].codepoint && gc->ext[h].codepoint != codepoint)
h = (h + 1) & mask;
if (!gc->ext[h].codepoint) {
gc->ext[h].codepoint = codepoint;
montauk::memset(&gc->ext[h].glyph, 0, sizeof(CachedGlyph));
gc->ext_count++;
}
return &gc->ext[h].glyph;
}
// Glyph for a Unicode codepoint. Codepoints the font lacks render as its
// .notdef box, so missing characters stay visible instead of vanishing.
CachedGlyph* get_glyph(GlyphCache* gc, int codepoint) {
if (codepoint < 0 || codepoint >= 256) return nullptr;
CachedGlyph* g = &gc->glyphs[codepoint];
if (codepoint < 0 || codepoint > 0x10FFFF) return nullptr;
CachedGlyph* g = codepoint < 256 ? &gc->glyphs[codepoint]
: ext_slot(gc, (uint32_t)codepoint);
if (!g) return nullptr;
if (g->loaded) return g;
g->loaded = true;
int font_codepoint = decode_single_byte_codepoint(codepoint);
int font_codepoint = codepoint;
int advance, lsb;
stbtt_GetCodepointHMetrics(&info, font_codepoint, &advance, &lsb);
g->advance = (int)(advance * gc->scale);
@@ -222,12 +262,34 @@ struct TrueTypeFont {
return g;
}
// Advance of one codepoint at a size (0 if the font is unusable).
int char_advance(int codepoint, int pixel_size) {
if (!valid) return 0;
CachedGlyph* g = get_glyph(get_cache(pixel_size), codepoint);
return g ? g->advance : 0;
}
// Width of the first `len` bytes of a UTF-8 string.
int measure_text_n(const char* text, int len, int pixel_size) {
if (!valid) return 0;
GlyphCache* gc = get_cache(pixel_size);
int w = 0;
for (int pos = 0; pos < len && text[pos];) {
int n;
uint32_t cp = montauk::utf8::decode(text + pos, len - pos, &n);
CachedGlyph* g = get_glyph(gc, (int)cp);
if (g) w += g->advance;
pos += n;
}
return w;
}
int measure_text(const char* text, int pixel_size) {
if (!valid) return 0;
GlyphCache* gc = get_cache(pixel_size);
int w = 0;
for (int i = 0; text[i]; i++) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
for (const char* p = text; *p;) {
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
if (g) w += g->advance;
}
return w;
@@ -253,8 +315,8 @@ struct TrueTypeFont {
uint32_t src_rgb = 0xFF000000 | ((uint32_t)color.r << 16) |
((uint32_t)color.g << 8) | color.b;
for (int i = 0; text[i]; i++) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
for (const char* p = text; *p;) {
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
if (!g) continue;
if (g->bitmap) {
@@ -325,8 +387,8 @@ struct TrueTypeFont {
int baseline = y + gc->ascent;
bool drew_pixel = false;
for (int i = 0; text[i]; i++) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
for (const char* p = text; *p;) {
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
if (!g) continue;
if (g->bitmap) {
@@ -378,8 +440,8 @@ struct TrueTypeFont {
int clip_x2 = clip_x + clip_w;
int clip_y2 = clip_y + clip_h;
for (int i = 0; text[i]; i++) {
CachedGlyph* g = get_glyph(gc, (unsigned char)text[i]);
for (const char* p = text; *p;) {
CachedGlyph* g = get_glyph(gc, (int)montauk::utf8::next(&p));
if (!g) continue;
if (g->bitmap) {
@@ -417,7 +479,7 @@ struct TrueTypeFont {
}
}
// Draw single character to buffer, returning advance width
// Draw one Unicode codepoint to buffer, returning advance width
int draw_char_to_buffer(uint32_t* pixels, int buf_w, int buf_h,
int x, int baseline, int codepoint,
Color color, GlyphCache* gc) {
+24 -27
View File
@@ -212,11 +212,7 @@ struct TextBox {
// Draw cursor if focused
if (focused) {
// Measure text up to cursor position for proportional fonts
char prefix[256];
for (int i = 0; i < cursor && i < 255; i++) prefix[i] = text[i];
prefix[cursor < 255 ? cursor : 255] = '\0';
int cx = tx + text_width(prefix);
int cx = tx + text_width_n(text, cursor);
draw_vline(fb, cx, ty, fh, cursor_color);
}
}
@@ -230,34 +226,35 @@ struct TextBox {
void handle_key(const montauk::abi::KeyEvent& key) {
if (!focused || !key.pressed) return;
char typed[montauk::utf8::MAX_BYTES];
int typed_len = montauk::utf8::key_utf8(key, typed);
if (key.ascii == '\b' || key.scancode == 0x0E) {
// Backspace
// Backspace: remove the whole character before the cursor
if (cursor > 0) {
for (int i = cursor - 1; i < text_len - 1; i++) {
text[i] = text[i + 1];
}
text_len--;
cursor--;
text[text_len] = '\0';
int start = montauk::utf8::prev_boundary(text, cursor);
int n = cursor - start;
for (int i = start; i <= text_len - n; i++)
text[i] = text[i + n];
text_len -= n;
cursor = start;
}
} else if ((unsigned char)key.ascii >= 32
&& (unsigned char)key.ascii != 127) {
// Printable character
if (text_len < 254) {
for (int i = text_len; i > cursor; i--) {
text[i] = text[i - 1];
}
text[cursor] = key.ascii;
cursor++;
text_len++;
text[text_len] = '\0';
} else if (typed_len > 0) {
// Printable character, inserted as UTF-8
if (text_len + typed_len <= 254) {
for (int i = text_len; i >= cursor; i--)
text[i + typed_len] = text[i];
for (int i = 0; i < typed_len; i++)
text[cursor + i] = typed[i];
cursor += typed_len;
text_len += typed_len;
}
} else if (key.scancode == 0x4B) {
} else if (montauk::nav_scancode(key) == 0x4B) {
// Left arrow
if (cursor > 0) cursor--;
} else if (key.scancode == 0x4D) {
cursor = montauk::utf8::prev_boundary(text, cursor);
} else if (montauk::nav_scancode(key) == 0x4D) {
// Right arrow
if (cursor < text_len) cursor++;
cursor = montauk::utf8::next_boundary(text, text_len, cursor);
}
}
};