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:
1 parent
32660ddd1a
commit
2aa6360b3d
101 files changed
+3733
-1639
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -227,6 +227,18 @@ struct DesktopState {
|
||||
montauk::abi::BatteryInfo battery_info;
|
||||
uint64_t battery_last_poll;
|
||||
|
||||
// Display brightness. Present only while the active output has a
|
||||
// controllable backlight (laptop panel, or a monitor answering DDC/CI).
|
||||
SvgIcon icon_brightness;
|
||||
bool brightness_present;
|
||||
bool brightness_popup_open;
|
||||
Rect brightness_icon_rect;
|
||||
int brightness_level; // 0-100, as shown on the slider
|
||||
bool brightness_dragging;
|
||||
int brightness_pending; // drag sample not yet sent, -1 none
|
||||
uint64_t brightness_last_send;
|
||||
uint64_t brightness_last_poll;
|
||||
|
||||
bool vol_popup_open;
|
||||
Rect vol_icon_rect;
|
||||
int vol_level; // 0-100
|
||||
|
||||
@@ -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
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user