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

+1 -1
View File
@@ -263,7 +263,7 @@ static int wp_char_advance_at(WordProcessorState* wp, int abs_pos, int layout_dp
wp_font_at(wp, abs_pos, layout_dpi, &font, &gc);
if (!font || !gc) return wp_points_to_pixels(6, layout_dpi);
CachedGlyph* g = font->get_glyph(gc, (unsigned char)ch);
CachedGlyph* g = font->get_glyph(gc, decode_single_byte_codepoint((unsigned char)ch));
return g ? g->advance : wp_points_to_pixels(6, layout_dpi);
}
+53 -37
View File
@@ -14,17 +14,46 @@ static void wp_set_status(WordProcessorState* wp, const char* msg) {
wp->status_msg[sizeof(wp->status_msg) - 1] = '\0';
}
// Documents store text as Windows-1252 (one byte per character, matching the
// WinAnsi fonts used for PDF export); the clipboard is UTF-8. Convert both ways.
static char* wp_cp1252_to_utf8_alloc(const char* text, int len, int* out_len) {
char* out = (char*)montauk::malloc(len * 3 + 1);
if (!out) return nullptr;
int o = 0;
for (int i = 0; i < len; i++)
o += montauk::utf8::encode(montauk::utf8::from_cp1252((uint8_t)text[i]), out + o);
out[o] = '\0';
*out_len = o;
return out;
}
// Characters with no Windows-1252 form become '?'.
static int wp_utf8_to_cp1252_inplace(char* text, int len) {
int o = 0;
for (int pos = 0; pos < len;) {
int n;
uint32_t cp = montauk::utf8::decode(text + pos, len - pos, &n);
pos += n;
uint8_t b = cp == 0 ? 0 : montauk::utf8::to_cp1252(cp);
text[o++] = b ? (char)b : '?';
}
text[o] = '\0';
return o;
}
static bool wp_copy_selection_to_clipboard(WordProcessorState* wp) {
int len = 0;
char* text = wp_selection_plain_text_alloc(wp, &len);
if (text == nullptr || len <= 0) {
if (text) montauk::mfree(text);
char* raw = wp_selection_plain_text_alloc(wp, &len);
if (raw == nullptr || len <= 0) {
if (raw) montauk::mfree(raw);
wp_set_status(wp, "Nothing selected");
return false;
}
bool ok = gui::clipboard_set_text(text, len);
montauk::mfree(text);
char* text = wp_cp1252_to_utf8_alloc(raw, len, &len);
montauk::mfree(raw);
bool ok = text && gui::clipboard_set_text(text, len);
if (text) montauk::mfree(text);
if (!ok) {
wp_set_status(wp, "Copy failed");
return false;
@@ -52,6 +81,7 @@ static bool wp_paste_from_clipboard(WordProcessorState* wp) {
wp_set_status(wp, "Clipboard is empty");
return false;
}
len = wp_utf8_to_cp1252_inplace(text, len);
bool ok = wp_replace_selection_with_text(wp, text, len);
montauk::mfree(text);
@@ -263,7 +293,7 @@ static int wp_hit_test_text(WordProcessorState* wp, int local_x, int local_y, in
for (int ci = 0; ci < to_check; ci++) {
char ch = r->text[ro + ci];
CachedGlyph* g = (ch >= 32 || ch < 0) && ch != '\n'
? font->get_glyph(gc, (unsigned char)ch) : nullptr;
? font->get_glyph(gc, decode_single_byte_codepoint((unsigned char)ch)) : nullptr;
int char_w = g ? g->advance : 0;
if (x + char_w / 2 > click_x) return best_abs;
@@ -611,32 +641,18 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
return;
}
if (key.ascii == '\b' || key.scancode == 0x0E) {
if (wp->pathbar_cursor > 0) {
for (int i = wp->pathbar_cursor - 1; i < wp->pathbar_len - 1; i++)
wp->pathbar_text[i] = wp->pathbar_text[i + 1];
wp->pathbar_len--;
wp->pathbar_cursor--;
wp->pathbar_text[wp->pathbar_len] = '\0';
}
montauk::utf8::erase_before(wp->pathbar_text, &wp->pathbar_len, &wp->pathbar_cursor);
return;
}
if (key.scancode == 0x4B) {
if (wp->pathbar_cursor > 0) wp->pathbar_cursor--;
if (montauk::nav_scancode(key) == 0x4B) {
wp->pathbar_cursor = montauk::utf8::prev_boundary(wp->pathbar_text, wp->pathbar_cursor);
return;
}
if (key.scancode == 0x4D) {
if (wp->pathbar_cursor < wp->pathbar_len) wp->pathbar_cursor++;
if (montauk::nav_scancode(key) == 0x4D) {
wp->pathbar_cursor = montauk::utf8::next_boundary(wp->pathbar_text, wp->pathbar_len, wp->pathbar_cursor);
return;
}
if ((unsigned char)key.ascii >= 32
&& (unsigned char)key.ascii != 127 && wp->pathbar_len < 254) {
for (int i = wp->pathbar_len; i > wp->pathbar_cursor; i--)
wp->pathbar_text[i] = wp->pathbar_text[i - 1];
wp->pathbar_text[wp->pathbar_cursor] = key.ascii;
wp->pathbar_cursor++;
wp->pathbar_len++;
wp->pathbar_text[wp->pathbar_len] = '\0';
}
montauk::utf8::insert_key(wp->pathbar_text, &wp->pathbar_len, 255, &wp->pathbar_cursor, key);
return;
}
@@ -736,22 +752,22 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
return;
}
if (key.ctrl && key.alt && key.scancode == 0x48) {
if (key.ctrl && key.alt && montauk::nav_scancode(key) == 0x48) {
wp_adjust_paragraph_spacing_before(wp, -WP_SPACE_STEP);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && key.alt && key.scancode == 0x50) {
if (key.ctrl && key.alt && montauk::nav_scancode(key) == 0x50) {
wp_adjust_paragraph_spacing_before(wp, WP_SPACE_STEP);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && key.shift && key.scancode == 0x48) {
if (key.ctrl && key.shift && montauk::nav_scancode(key) == 0x48) {
wp_adjust_paragraph_spacing_after(wp, -WP_SPACE_STEP);
wp_history_checkpoint(wp);
return;
}
if (key.ctrl && key.shift && key.scancode == 0x50) {
if (key.ctrl && key.shift && montauk::nav_scancode(key) == 0x50) {
wp_adjust_paragraph_spacing_after(wp, WP_SPACE_STEP);
wp_history_checkpoint(wp);
return;
@@ -783,21 +799,21 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
return;
}
if (key.scancode == 0x48) {
if (montauk::nav_scancode(key) == 0x48) {
if (key.shift) wp_start_selection(wp);
else wp_clear_selection(wp);
wp_cursor_up(wp);
if (key.shift) wp_update_selection_to_cursor(wp);
return;
}
if (key.scancode == 0x50) {
if (montauk::nav_scancode(key) == 0x50) {
if (key.shift) wp_start_selection(wp);
else wp_clear_selection(wp);
wp_cursor_down(wp);
if (key.shift) wp_update_selection_to_cursor(wp);
return;
}
if (key.scancode == 0x4B) {
if (montauk::nav_scancode(key) == 0x4B) {
if (key.shift) wp_start_selection(wp);
else if (wp->has_selection) {
int s, e;
@@ -810,7 +826,7 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
if (key.shift) wp_update_selection_to_cursor(wp);
return;
}
if (key.scancode == 0x4D) {
if (montauk::nav_scancode(key) == 0x4D) {
if (key.shift) wp_start_selection(wp);
else if (wp->has_selection) {
int s, e;
@@ -824,7 +840,7 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
return;
}
if (key.scancode == 0x47) {
if (montauk::nav_scancode(key) == 0x47) {
if (key.shift) wp_start_selection(wp);
else wp_clear_selection(wp);
int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
@@ -834,7 +850,7 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
if (key.shift) wp_update_selection_to_cursor(wp);
return;
}
if (key.scancode == 0x4F) {
if (montauk::nav_scancode(key) == 0x4F) {
if (key.shift) wp_start_selection(wp);
else wp_clear_selection(wp);
int abs = wp_abs_pos(wp, wp->cursor_run, wp->cursor_offset);
@@ -858,7 +874,7 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
return;
}
if (key.scancode == 0x53) {
if (montauk::nav_scancode(key) == 0x53) {
if (wp->has_selection) wp_delete_selection(wp);
else wp_delete_char(wp);
wp_history_checkpoint(wp);
+3 -2
View File
@@ -217,7 +217,7 @@ static void wp_draw_print_list_marker(Canvas& c, WordProcessorState* wp, WrapLin
if (!font) font = g_ui_font;
int th = text_height(font, font_px);
int top_y = py + (wl->height - th) / 2;
char bullet[2] = { '\x95', '\0' };
const char* bullet = "\u2022";
draw_text(c, font, bullet_x, top_y, bullet, colors::BLACK, font_px);
return;
}
@@ -293,7 +293,8 @@ static bool wp_encode_print_page(WordProcessorState* wp,
if (ch != '\n' && (ch >= 32 || ch < 0)) {
int adv = font->draw_char_to_buffer(
c.pixels, c.w, c.h, x, baseline, (unsigned char)ch, colors::BLACK, gc);
c.pixels, c.w, c.h, x, baseline,
decode_single_byte_codepoint((unsigned char)ch), colors::BLACK, gc);
x += adv;
}
char_idx++;
+13 -8
View File
@@ -111,7 +111,8 @@ static void wp_fit_ui_text(const char* src, char* out, int out_len, int max_w) {
if (text_width(g_ui_font, out, fonts::UI_SIZE) <= max_w) return;
int slen = montauk::slen(src);
for (int keep = slen; keep > 1; keep--) {
for (int keep = slen; keep > 1;
keep = slen - montauk::utf8::next_boundary(src, slen, slen - keep)) {
char candidate[256];
candidate[0] = '.';
candidate[1] = '.';
@@ -138,7 +139,8 @@ static void wp_fit_ui_text_end(const char* src, char* out, int out_len, int max_
out[out_len - 1] = '\0';
if (text_width(g_ui_font, out, fonts::UI_SIZE) <= max_w) return;
for (int keep = montauk::slen(src); keep > 0; keep--) {
for (int keep = montauk::slen(src); keep > 0;
keep = montauk::utf8::prev_boundary(src, keep)) {
char candidate[256];
if (keep > (int)sizeof(candidate) - 3) continue;
montauk::memcpy(candidate, src, keep);
@@ -225,7 +227,7 @@ static void wp_draw_list_marker(Canvas& c, WordProcessorState* wp, WrapLine* wl,
if (!font) font = g_ui_font;
int th = text_height(font, bullet_size);
int top_y = py + (wl->height - th) / 2;
char bullet[2] = { '\x95', '\0' };
const char* bullet = "\u2022";
draw_text(c, font, bullet_x, top_y, bullet, colors::TEXT_COLOR, bullet_size);
return;
}
@@ -346,7 +348,7 @@ void wp_render() {
Color special_bg = wp->special_char_flyout_open ? btn_active : btn_bg;
c.fill_rounded_rect(WP_BTN_SECTION_X, 6, 24, 24, 3, special_bg);
{
char section[2] = { (char)0xA7, '\0' };
const char* section = "\u00A7";
TrueTypeFont* font = wp_get_font(wp_default_font_id(), 0);
draw_text(c, font ? font : g_ui_font, WP_BTN_SECTION_X + 6, 8, section, colors::TEXT_COLOR, fonts::UI_SIZE);
}
@@ -355,7 +357,7 @@ void wp_render() {
Color pageview_bg = wp->page_view_mode ? btn_active : btn_bg;
c.fill_rounded_rect(WP_BTN_PAGEVIEW_X, 6, 24, 24, 3, pageview_bg);
wp_draw_ui_icon_button(c, WP_BTN_PAGEVIEW_X, 6, 24, 24, g_icon_page_view, "\xA7",
wp_draw_ui_icon_button(c, WP_BTN_PAGEVIEW_X, 6, 24, 24, g_icon_page_view, "\u00A7",
pageview_bg, colors::TEXT_COLOR);
c.hline(0, WP_TOOLBAR_H - 1, c.w, colors::BORDER);
@@ -506,7 +508,7 @@ void wp_render() {
int char_adv = 0;
if (ch != '\n' && (ch >= 32 || ch < 0)) {
CachedGlyph* g = font->get_glyph(gc, (unsigned char)ch);
CachedGlyph* g = font->get_glyph(gc, decode_single_byte_codepoint((unsigned char)ch));
char_adv = g ? g->advance : 8;
}
@@ -558,7 +560,8 @@ void wp_render() {
Color text_col = (wp->has_selection && abs_ch >= sel_s && abs_ch < sel_e)
? Color::from_rgb(0x10, 0x10, 0x10) : colors::TEXT_COLOR;
int adv = font->draw_char_to_buffer(
c.pixels, c.w, c.h, x, baseline, (unsigned char)ch, text_col, gc);
c.pixels, c.w, c.h, x, baseline,
decode_single_byte_codepoint((unsigned char)ch), text_col, gc);
x += adv;
} else {
x += char_adv;
@@ -717,7 +720,9 @@ void wp_render() {
c.rect(dx, dy, dw, dh, colors::BORDER);
for (int i = 0; i < WP_SPECIAL_CHAR_OPTION_COUNT; i++) {
int iy = dy + 2 + i * WP_SPECIAL_CHAR_ROW_H;
char glyph[2] = { (char)WP_SPECIAL_CHAR_OPTIONS[i].code, '\0' };
char glyph[5];
glyph[montauk::utf8::encode(montauk::utf8::from_cp1252(
WP_SPECIAL_CHAR_OPTIONS[i].code), glyph)] = '\0';
c.fill_rect(dx + 2, iy, dw - 4, WP_SPECIAL_CHAR_ROW_H - 2, colors::MENU_BG);
draw_text(c, glyph_font ? glyph_font : g_ui_font,
dx + 8, iy + (WP_SPECIAL_CHAR_ROW_H - sfh) / 2,
@@ -193,6 +193,10 @@ struct WPFontTable {
bool loaded;
};
// Document text is Windows-1252, one byte per character, unlike the rest of
// MontaukOS (UTF-8). It matches the MWP1 file format and the WinAnsi-encoded
// fonts of PDF export. Convert at the edges: decode_single_byte_codepoint()
// for glyphs, and UTF-8 <-> 1252 for the clipboard (input.cpp).
struct StyledRun {
char* text;
int len;