From 81ea21e746f694ac9a8f9412caef3ad6f7f83f4a Mon Sep 17 00:00:00 2001 From: boreddevnl Date: Thu, 23 Apr 2026 21:59:42 +0200 Subject: [PATCH] fix(input): update boredword, browser, grapher and libwidget to support UTF8 --- src/userland/gui/boredword.c | 181 +++++++++++++++++++++----------- src/userland/gui/browser.c | 198 ++++++++++++++++++++--------------- src/userland/gui/grapher.c | 12 ++- src/userland/gui/settings.c | 44 ++++---- src/userland/gui/terminal.c | 4 +- src/wm/libwidget.c | 96 ++++++++++++----- src/wm/libwidget.h | 4 +- src/wm/utf-8.c | 115 ++++++++++++++++++++ src/wm/utf-8.h | 25 +++++ 9 files changed, 477 insertions(+), 202 deletions(-) create mode 100644 src/wm/utf-8.c create mode 100644 src/wm/utf-8.h diff --git a/src/userland/gui/boredword.c b/src/userland/gui/boredword.c index 8808100..25baaf4 100644 --- a/src/userland/gui/boredword.c +++ b/src/userland/gui/boredword.c @@ -7,6 +7,7 @@ #include "libc/libui.h" #include "libc/stdlib.h" #include "../../wm/libwidget.h" +#include "utf-8.h" #include #define COLOR_DARK_PANEL 0xFF202020 @@ -243,23 +244,24 @@ static void update_formatting_state(void) { } } -static void handle_arrows(ui_window_t win, char c) { - if (c == 17) { +static void handle_arrows(ui_window_t win, uint32_t c) { + if (c == 17) { // UP if (cursor_para > 0) { cursor_para--; cursor_run = paragraphs[cursor_para].run_count - 1; if (cursor_run < 0) cursor_run = 0; cursor_pos = paragraphs[cursor_para].runs[cursor_run].len; } - } else if (c == 18) { + } else if (c == 18) { // DOWN if (cursor_para < para_count - 1) { cursor_para++; cursor_run = 0; cursor_pos = 0; } - } else if (c == 19) { + } else if (c == 19) { // LEFT if (cursor_pos > 0) { - cursor_pos--; + const char *prev = text_prev_utf8(paragraphs[cursor_para].runs[cursor_run].text, paragraphs[cursor_para].runs[cursor_run].text + cursor_pos); + cursor_pos = (int)(prev - paragraphs[cursor_para].runs[cursor_run].text); } else if (cursor_run > 0) { cursor_run--; cursor_pos = paragraphs[cursor_para].runs[cursor_run].len; @@ -269,9 +271,10 @@ static void handle_arrows(ui_window_t win, char c) { if (cursor_run < 0) cursor_run = 0; cursor_pos = paragraphs[cursor_para].runs[cursor_run].len; } - } else if (c == 20) { + } else if (c == 20) { // RIGHT if (cursor_pos < paragraphs[cursor_para].runs[cursor_run].len) { - cursor_pos++; + const char *next = text_next_utf8(paragraphs[cursor_para].runs[cursor_run].text + cursor_pos); + cursor_pos = (int)(next - paragraphs[cursor_para].runs[cursor_run].text); } else if (cursor_run < paragraphs[cursor_para].run_count - 1) { cursor_run++; cursor_pos = 0; @@ -357,7 +360,7 @@ static void delete_selection(void) { file_modified = 1; } -static void insert_char(ui_window_t win, char c) { +static void insert_char(ui_window_t win, uint32_t c, int legacy) { _Bool has_selection = 0; if (sel_start_para != -1 && sel_end_para != -1) { if (!(sel_start_para == sel_end_para && sel_start_run == sel_end_run && sel_start_pos == sel_end_pos)) { @@ -367,7 +370,7 @@ static void insert_char(ui_window_t win, char c) { if (has_selection) { delete_selection(); - if (c == '\b') return; + if (legacy == '\b' || legacy == 127) return; } else { if (sel_start_para != -1) { sel_start_para = -1; @@ -375,20 +378,23 @@ static void insert_char(ui_window_t win, char c) { } } - if (c < 32 && c != '\n' && c != '\b') { - if (c >= 17 && c <= 20) { - handle_arrows(win, c); - } + // Handle special legacy keys + if (legacy >= 17 && legacy <= 20) { + handle_arrows(win, legacy); return; } - if (c == '\b') { + if (legacy == '\b') { save_undo_state(); if (cursor_pos > 0) { TextRun *r = ¶graphs[cursor_para].runs[cursor_run]; - for(int i=cursor_pos-1; ilen; i++) r->text[i] = r->text[i+1]; - r->len--; - cursor_pos--; + const char *prev = text_prev_utf8(r->text, r->text + cursor_pos); + int char_len = (int)((r->text + cursor_pos) - prev); + + for(int i=cursor_pos-char_len; ilen-char_len; i++) r->text[i] = r->text[i+char_len]; + r->len -= char_len; + cursor_pos -= char_len; + r->text[r->len] = 0; if (r->len == 0 && paragraphs[cursor_para].run_count > 1) { for(int i = cursor_run; i < paragraphs[cursor_para].run_count - 1; i++) { @@ -411,7 +417,10 @@ static void insert_char(ui_window_t win, char c) { if (cursor_run < 0) cursor_run = 0; TextRun *r = ¶graphs[cursor_para].runs[cursor_run]; if (r->len > 0) { - r->len--; + const char *prev = text_prev_utf8(r->text, r->text + r->len); + int char_len = (int)((r->text + r->len) - prev); + r->len -= char_len; + r->text[r->len] = 0; cursor_pos = r->len; if (r->len == 0 && paragraphs[cursor_para].run_count > 1) { @@ -453,7 +462,7 @@ static void insert_char(ui_window_t win, char c) { return; } - if (c == '\n') { + if (legacy == '\n') { save_undo_state(); if (para_count >= MAX_PARAGRAPHS) return; for(int i=para_count; i>cursor_para+1; i--) paragraphs[i] = paragraphs[i-1]; @@ -519,14 +528,18 @@ static void insert_char(ui_window_t win, char c) { r->color = current_text_color; } - if (c == ' ') save_undo_state(); + if (legacy == ' ') save_undo_state(); - if (r->len < MAX_RUN_TEXT - 1) { - for(int i=r->len; i>cursor_pos; i--) r->text[i] = r->text[i-1]; - r->text[cursor_pos] = c; - r->len++; - cursor_pos++; - file_modified = 1; + char utf8[4]; + if (c >= 32 && c != 127) { + int clen = text_encode_utf8(c, utf8); + if (clen > 0 && r->len + clen < MAX_RUN_TEXT) { + for(int i=r->len; i>cursor_pos; i--) r->text[i + clen - 1] = r->text[i-1]; + for(int i=0; itext[cursor_pos + i] = utf8[i]; + r->len += clen; + cursor_pos += clen; + file_modified = 1; + } } ensure_cursor_visible(win); } @@ -945,12 +958,14 @@ static void load_file(ui_window_t win, const char* path) { i++; while(i < size && buf[i] != ')') { if (buf[i] == '\\' && i+1 < size) i++; - insert_char(win, buf[i]); - i++; + int adv; + uint32_t cp = text_decode_utf8(buf + i, &adv); + insert_char(win, cp, (int)buf[i]); + i += adv; } } else if ((buf[i] == 'T' && buf[i+1] == 'd') || (buf[i] == 'T' && buf[i+1] == 'm')) { - insert_char(win, '\n'); + insert_char(win, '\n', '\n'); i += 2; } else { @@ -958,8 +973,15 @@ static void load_file(ui_window_t win, const char* path) { } } } else { - for(int i=0; i max_h) max_h = fh; while(c_idx < run->len) { - char buf[2] = {run->text[c_idx], 0}; + int adv; + text_decode_utf8(run->text + c_idx, &adv); + char buf[5]; + for (int k = 0; k < adv; k++) buf[k] = run->text[c_idx + k]; + buf[adv] = 0; + int cw = ui_get_string_width_scaled(buf, run->font_size); if (run->text[c_idx] == ' ') { @@ -1184,7 +1211,7 @@ static void draw_document(ui_window_t win) { break; } line_w += cw; - c_idx++; + c_idx += adv; } if (c_idx < run->len) break; @@ -1242,9 +1269,11 @@ static void draw_document(ui_window_t win) { else chars_to_draw = run->len - d_char; if (p == cursor_para && d_run == cursor_run && cursor_pos >= d_char && cursor_pos <= d_char + chars_to_draw) { - char sub[128]; - for(int i=0; itext[d_char + i]; - sub[cursor_pos - d_char] = 0; + char sub[512]; + int len_before = cursor_pos - d_char; + if (len_before > 511) len_before = 511; + for(int i=0; itext[d_char + i]; + sub[len_before] = 0; line_cursor_x = cur_x + ui_get_string_width_scaled(sub, run->font_size); } @@ -1257,9 +1286,15 @@ static void draw_document(ui_window_t win) { int text_y = cur_y + y_offset; if (text_y + run_h > 40 && text_y < win_h) { - for(int i=0; itext[d_char + i], 0}; - _Bool in_sel = is_in_selection(p, d_run, d_char + i); + int c_offset_local = 0; + while (c_offset_local < chars_to_draw) { + int adv; + text_decode_utf8(run->text + d_char + c_offset_local, &adv); + char buf[5]; + for (int k = 0; k < adv; k++) buf[k] = run->text[d_char + c_offset_local + k]; + buf[adv] = 0; + + _Bool in_sel = is_in_selection(p, d_run, d_char + c_offset_local); uint32_t text_col = in_sel ? COLOR_WHITE : run->color; int cw = ui_get_string_width_scaled(buf, run->font_size); @@ -1281,11 +1316,14 @@ static void draw_document(ui_window_t win) { } } cur_x += cw; + c_offset_local += adv; } } else { - char buf[128]; - for(int i=0; itext[d_char + i]; - buf[chars_to_draw] = 0; + char buf[512]; + int len_to_copy = chars_to_draw; + if (len_to_copy > 511) len_to_copy = 511; + for(int i=0; itext[d_char + i]; + buf[len_to_copy] = 0; cur_x += ui_get_string_width_scaled(buf, run->font_size); } } @@ -1367,12 +1405,17 @@ static void ensure_cursor_visible(ui_window_t win) { if (fh > max_h) max_h = fh; while(c_idx < run->len) { - char buf[2] = {run->text[c_idx], 0}; + int adv; + text_decode_utf8(run->text + c_idx, &adv); + char buf[5]; + for (int k = 0; k < adv; k++) buf[k] = run->text[c_idx + k]; + buf[adv] = 0; + int cw = ui_get_string_width_scaled(buf, run->font_size); if (run->text[c_idx] == ' ') { last_space_run = r_idx; last_space_char = c_idx; last_space_w = line_w + cw; } if (line_w + cw > page_w - 20) break; line_w += cw; - c_idx++; + c_idx += adv; } if (c_idx < run->len) break; r_idx++; c_idx = 0; @@ -1692,12 +1735,17 @@ static void handle_click(ui_window_t win, int x, int y) { int fh = ui_get_font_height_scaled(run->font_size); if (fh > max_h) max_h = fh; while(c_idx < run->len) { - char buf[2] = {run->text[c_idx], 0}; + int adv; + text_decode_utf8(run->text + c_idx, &adv); + char buf[5]; + for (int k = 0; k < adv; k++) buf[k] = run->text[c_idx + k]; + buf[adv] = 0; + int cw = ui_get_string_width_scaled(buf, run->font_size); if (run->text[c_idx] == ' ') { last_space_run = r_idx; last_space_char = c_idx; last_space_w = line_w + cw; } if (line_w + cw > page_w - 20) break; line_w += cw; - c_idx++; + c_idx += adv; } if (c_idx < run->len) break; r_idx++; c_idx = 0; @@ -1731,31 +1779,38 @@ static void handle_click(ui_window_t win, int x, int y) { TextRun *run = ¶->runs[d_run]; set_active_font(win, run->font_idx); int chars_to_draw = (d_run == end_run) ? (end_char - d_char) : (run->len - d_char); - for(int i=0; itext[d_char + i], 0}; + int cur_c_offset = 0; + while(cur_c_offset < chars_to_draw) { + int adv; + text_decode_utf8(run->text + d_char + cur_c_offset, &adv); + char buf[5]; + for (int k = 0; k < adv; k++) buf[k] = run->text[d_char + cur_c_offset + k]; + buf[adv] = 0; + int cw = ui_get_string_width_scaled(buf, run->font_size); if (target_x >= cur_x && target_x < cur_x + cw/2) { - cursor_para = p; cursor_run = d_run; cursor_pos = d_char + i; + cursor_para = p; cursor_run = d_run; cursor_pos = d_char + cur_c_offset; if (selection_started) { - sel_start_para = p; sel_start_run = d_run; sel_start_pos = d_char + i; + sel_start_para = p; sel_start_run = d_run; sel_start_pos = d_char + cur_c_offset; sel_end_para = -1; selection_started = 0; } else if (is_dragging) { - update_selection(p, d_run, d_char + i); + update_selection(p, d_run, d_char + cur_c_offset); } return; } else if (target_x >= cur_x + cw/2 && target_x < cur_x + cw) { - cursor_para = p; cursor_run = d_run; cursor_pos = d_char + i + 1; + cursor_para = p; cursor_run = d_run; cursor_pos = d_char + cur_c_offset + adv; if (selection_started) { - sel_start_para = p; sel_start_run = d_run; sel_start_pos = d_char + i + 1; + sel_start_para = p; sel_start_run = d_run; sel_start_pos = d_char + cur_c_offset + adv; sel_end_para = -1; selection_started = 0; } else if (is_dragging) { - update_selection(p, d_run, d_char + i + 1); + update_selection(p, d_run, d_char + cur_c_offset + adv); } return; } cur_x += cw; + cur_c_offset += adv; } d_char = 0; d_run++; } @@ -1902,16 +1957,22 @@ int main(int argc, char **argv) { } else if (ev.type == GUI_EVENT_CLICK) { needs_repaint = 1; } else if (ev.type == GUI_EVENT_KEY) { + uint32_t cp = (uint32_t)ev.arg4; if (active_dialog == 1) { - char c = (char)ev.arg1; - if (c == '\b' && dialog_input_len > 0) { - dialog_input[--dialog_input_len] = 0; - } else if (c >= 32 && c < 127 && dialog_input_len < 127) { - dialog_input[dialog_input_len++] = c; + if (cp == '\b' && dialog_input_len > 0) { + const char *prev = text_prev_utf8(dialog_input, dialog_input + dialog_input_len); + dialog_input_len = (int)(prev - dialog_input); dialog_input[dialog_input_len] = 0; + } else if (cp >= 32 && cp != 127) { + char utf8[4]; + int clen = text_encode_utf8(cp, utf8); + if (clen > 0 && dialog_input_len + clen < 255) { + for(int i=0; i #include #include "libc/stdlib.h" +#include "utf-8.h" static int win_w = 1280; static int win_h = 960; @@ -782,84 +783,73 @@ static void decode_html_entities(char *str) { if (str_istarts_with(src, ">")) { *dst++ = '>'; src += 4; continue; } if (str_istarts_with(src, "'")) { *dst++ = '\''; src += 6; continue; } if (str_istarts_with(src, " ")) { *dst++ = ' '; src += 6; continue; } - if (str_istarts_with(src, "—")) { *dst++ = (char)128; src += 7; continue; } - if (str_istarts_with(src, "&mdash")) { *dst++ = (char)128; src += 6; continue; } - if (str_istarts_with(src, "–")) { *dst++ = (char)129; src += 7; continue; } - if (str_istarts_with(src, "&ndash")) { *dst++ = (char)129; src += 6; continue; } - if (str_istarts_with(src, "•")) { *dst++ = (char)130; src += 6; continue; } - if (str_istarts_with(src, "&bull")) { *dst++ = (char)130; src += 5; continue; } - if (str_istarts_with(src, "…")){ *dst++ = (char)131; src += 8; continue; } - if (str_istarts_with(src, "&hellip")){ *dst++ = (char)131; src += 7; continue; } - if (str_istarts_with(src, "™")) { *dst++ = (char)132; src += 7; continue; } - if (str_istarts_with(src, "€")) { *dst++ = (char)133; src += 6; continue; } - if (str_istarts_with(src, "·")){ *dst++ = (char)134; src += 8; continue; } + if (str_istarts_with(src, "—")) { dst += text_encode_utf8(8212, dst); src += 7; continue; } + if (str_istarts_with(src, "&mdash")) { dst += text_encode_utf8(8212, dst); src += 6; continue; } + if (str_istarts_with(src, "–")) { dst += text_encode_utf8(8211, dst); src += 7; continue; } + if (str_istarts_with(src, "&ndash")) { dst += text_encode_utf8(8211, dst); src += 6; continue; } + if (str_istarts_with(src, "•")) { dst += text_encode_utf8(8226, dst); src += 6; continue; } + if (str_istarts_with(src, "&bull")) { dst += text_encode_utf8(8226, dst); src += 5; continue; } + if (str_istarts_with(src, "…")){ dst += text_encode_utf8(8230, dst); src += 8; continue; } + if (str_istarts_with(src, "&hellip")){ dst += text_encode_utf8(8230, dst); src += 7; continue; } + if (str_istarts_with(src, "™")) { dst += text_encode_utf8(8482, dst); src += 7; continue; } + if (str_istarts_with(src, "€")) { dst += text_encode_utf8(8364, dst); src += 6; continue; } + if (str_istarts_with(src, "·")){ dst += text_encode_utf8(183, dst); src += 8; continue; } if (str_istarts_with(src, "‘")) { *dst++ = '\''; src += 7; continue; } if (str_istarts_with(src, "’")) { *dst++ = '\''; src += 7; continue; } if (str_istarts_with(src, "“")) { *dst++ = '\"'; src += 7; continue; } if (str_istarts_with(src, "”")) { *dst++ = '\"'; src += 7; continue; } - if (str_istarts_with(src, "¡")) { *dst++ = (char)161; src += 7; continue; } - if (str_istarts_with(src, "¢")) { *dst++ = (char)162; src += 6; continue; } - if (str_istarts_with(src, "£")) { *dst++ = (char)163; src += 7; continue; } - if (str_istarts_with(src, "¥")) { *dst++ = (char)165; src += 5; continue; } - if (str_istarts_with(src, "©")) { *dst++ = (char)169; src += 6; continue; } - if (str_istarts_with(src, "®")) { *dst++ = (char)174; src += 5; continue; } - if (str_istarts_with(src, "°")) { *dst++ = (char)176; src += 5; continue; } - if (str_istarts_with(src, "á")) { *dst++ = (char)225; src += 8; continue; } - if (str_istarts_with(src, "é")) { *dst++ = (char)233; src += 8; continue; } - if (str_istarts_with(src, "í")) { *dst++ = (char)237; src += 8; continue; } - if (str_istarts_with(src, "ó")) { *dst++ = (char)243; src += 8; continue; } - if (str_istarts_with(src, "ú")) { *dst++ = (char)250; src += 8; continue; } - if (str_istarts_with(src, "ñ")) { *dst++ = (char)241; src += 8; continue; } - if (str_istarts_with(src, "ü")) { *dst++ = (char)252; src += 6; continue; } - if (str_istarts_with(src, "¿")) { *dst++ = (char)191; src += 8; continue; } - if (str_istarts_with(src, "À")) { *dst++ = (char)192; src += 8; continue; } - if (str_istarts_with(src, "Á")) { *dst++ = (char)193; src += 8; continue; } - if (str_istarts_with(src, "×")) { *dst++ = (char)215; src += 7; continue; } - if (str_istarts_with(src, "÷")) { *dst++ = (char)247; src += 8; continue; } - if (str_istarts_with(src, "±")) { *dst++ = (char)177; src += 8; continue; } - if (str_istarts_with(src, "µ")) { *dst++ = (char)181; src += 7; continue; } - if (str_istarts_with(src, "¶")) { *dst++ = (char)182; src += 6; continue; } - if (str_istarts_with(src, "¦")) { *dst++ = (char)166; src += 8; continue; } - if (str_istarts_with(src, "§")) { *dst++ = (char)167; src += 6; continue; } - if (str_istarts_with(src, "¨")) { *dst++ = (char)168; src += 5; continue; } - if (str_istarts_with(src, "ª")) { *dst++ = (char)170; src += 6; continue; } - if (str_istarts_with(src, "«")) { *dst++ = (char)171; src += 7; continue; } - if (str_istarts_with(src, "¬")) { *dst++ = (char)172; src += 5; continue; } + if (str_istarts_with(src, "¡")) { dst += text_encode_utf8(161, dst); src += 7; continue; } + if (str_istarts_with(src, "¢")) { dst += text_encode_utf8(162, dst); src += 6; continue; } + if (str_istarts_with(src, "£")) { dst += text_encode_utf8(163, dst); src += 7; continue; } + if (str_istarts_with(src, "¥")) { dst += text_encode_utf8(165, dst); src += 5; continue; } + if (str_istarts_with(src, "©")) { dst += text_encode_utf8(169, dst); src += 6; continue; } + if (str_istarts_with(src, "®")) { dst += text_encode_utf8(174, dst); src += 5; continue; } + if (str_istarts_with(src, "°")) { dst += text_encode_utf8(176, dst); src += 5; continue; } + if (str_istarts_with(src, "á")) { dst += text_encode_utf8(225, dst); src += 8; continue; } + if (str_istarts_with(src, "é")) { dst += text_encode_utf8(233, dst); src += 8; continue; } + if (str_istarts_with(src, "í")) { dst += text_encode_utf8(237, dst); src += 8; continue; } + if (str_istarts_with(src, "ó")) { dst += text_encode_utf8(243, dst); src += 8; continue; } + if (str_istarts_with(src, "ú")) { dst += text_encode_utf8(250, dst); src += 8; continue; } + if (str_istarts_with(src, "ñ")) { dst += text_encode_utf8(241, dst); src += 8; continue; } + if (str_istarts_with(src, "ü")) { dst += text_encode_utf8(252, dst); src += 6; continue; } + if (str_istarts_with(src, "¿")) { dst += text_encode_utf8(191, dst); src += 8; continue; } + if (str_istarts_with(src, "À")) { dst += text_encode_utf8(192, dst); src += 8; continue; } + if (str_istarts_with(src, "Á")) { dst += text_encode_utf8(193, dst); src += 8; continue; } + if (str_istarts_with(src, "×")) { dst += text_encode_utf8(215, dst); src += 7; continue; } + if (str_istarts_with(src, "÷")) { dst += text_encode_utf8(247, dst); src += 8; continue; } + if (str_istarts_with(src, "±")) { dst += text_encode_utf8(177, dst); src += 8; continue; } + if (str_istarts_with(src, "µ")) { dst += text_encode_utf8(181, dst); src += 7; continue; } + if (str_istarts_with(src, "¶")) { dst += text_encode_utf8(182, dst); src += 6; continue; } + if (str_istarts_with(src, "¦")) { dst += text_encode_utf8(166, dst); src += 8; continue; } + if (str_istarts_with(src, "§")) { dst += text_encode_utf8(167, dst); src += 6; continue; } + if (str_istarts_with(src, "¨")) { dst += text_encode_utf8(168, dst); src += 5; continue; } + if (str_istarts_with(src, "ª")) { dst += text_encode_utf8(170, dst); src += 6; continue; } + if (str_istarts_with(src, "«")) { dst += text_encode_utf8(171, dst); src += 7; continue; } + if (str_istarts_with(src, "¬")) { dst += text_encode_utf8(172, dst); src += 5; continue; } if (str_istarts_with(src, "­")) { src += 5; continue; } // Soft hyphen, ignore - if (str_istarts_with(src, "¯")) { *dst++ = (char)175; src += 6; continue; } - if (str_istarts_with(src, "²")) { *dst++ = (char)178; src += 6; continue; } - if (str_istarts_with(src, "³")) { *dst++ = (char)179; src += 6; continue; } - if (str_istarts_with(src, "´")) { *dst++ = (char)180; src += 7; continue; } - if (str_istarts_with(src, "¸")) { *dst++ = (char)184; src += 7; continue; } - if (str_istarts_with(src, "¹")) { *dst++ = (char)185; src += 6; continue; } - if (str_istarts_with(src, "º")) { *dst++ = (char)186; src += 6; continue; } - if (str_istarts_with(src, "»")) { *dst++ = (char)187; src += 7; continue; } - if (str_istarts_with(src, "¼")) { *dst++ = (char)188; src += 8; continue; } - if (str_istarts_with(src, "½")) { *dst++ = (char)189; src += 8; continue; } - if (str_istarts_with(src, "¾")) { *dst++ = (char)190; src += 8; continue; } + if (str_istarts_with(src, "¯")) { dst += text_encode_utf8(175, dst); src += 6; continue; } + if (str_istarts_with(src, "²")) { dst += text_encode_utf8(178, dst); src += 6; continue; } + if (str_istarts_with(src, "³")) { dst += text_encode_utf8(179, dst); src += 6; continue; } + if (str_istarts_with(src, "´")) { dst += text_encode_utf8(180, dst); src += 7; continue; } + if (str_istarts_with(src, "¸")) { dst += text_encode_utf8(184, dst); src += 7; continue; } + if (str_istarts_with(src, "¹")) { dst += text_encode_utf8(185, dst); src += 6; continue; } + if (str_istarts_with(src, "º")) { dst += text_encode_utf8(186, dst); src += 6; continue; } + if (str_istarts_with(src, "»")) { dst += text_encode_utf8(187, dst); src += 7; continue; } + if (str_istarts_with(src, "¼")) { dst += text_encode_utf8(188, dst); src += 8; continue; } + if (str_istarts_with(src, "½")) { dst += text_encode_utf8(189, dst); src += 8; continue; } + if (str_istarts_with(src, "¾")) { dst += text_encode_utf8(190, dst); src += 8; continue; } if (src[1] == '#') { int val = 0; char *end = NULL; if (src[2] == 'x' || src[2] == 'X') { - val = strtol(src + 3, &end, 16); + val = (int)strtol(src + 3, &end, 16); } else { - val = strtol(src + 2, &end, 10); + val = (int)strtol(src + 2, &end, 10); } if (end && *end == ';' && end > src + 2) { - if (val == 8211) val = 129; // – - else if (val == 8212) val = 128; // — - else if (val == 8226) val = 130; // • - else if (val == 8230) val = 131; // … - else if (val == 8482) val = 132; // ™ - else if (val == 8364) val = 133; // € - else if (val == 183) val = 134; // · - else if (val == 8216 || val == 8217) val = '\''; - else if (val == 8220 || val == 8221) val = '\"'; - else if (val == 160) val = ' '; - - if (val > 0 && val < 256) { - *dst++ = (char)val; + if (val > 0) { + dst += text_encode_utf8((uint32_t)val, dst); src = end + 1; continue; } @@ -1810,7 +1800,7 @@ int main(int argc, char **argv) { if (ev.type != GUI_EVENT_CLICK && ev.type != GUI_EVENT_MOUSE_DOWN) continue; if (my < URL_BAR_H) { - if (widget_textbox_handle_mouse(&url_tb, mx, my, is_click, NULL)) { + if (widget_textbox_handle_mouse(&browser_ctx, &url_tb, mx, my, is_click, NULL)) { focused_element = -1; needs_repaint = true; continue; @@ -1994,26 +1984,46 @@ int main(int argc, char **argv) { if (!found) { focused_element = -1; needs_repaint = true; } } else if (ev.type == GUI_EVENT_KEY || ev.type == GUI_EVENT_KEYUP) { if (ev.type == GUI_EVENT_KEYUP) continue; + uint32_t cp = (uint32_t)ev.arg4; char c = (char)ev.arg1; if (focused_element == -1) { - if (c == 13 || c == 10) { + if (cp == 13 || cp == 10) { if (history_count < HISTORY_MAX) { int j=0; while(url_input_buffer[j]) { history_stack[history_count][j] = url_input_buffer[j]; j++; } history_stack[history_count][j] = 0; history_count++; } navigate(url_input_buffer); scroll_y = 0; needs_repaint = true; } - else if (c == 19) { if (url_cursor > 0) url_cursor--; } - else if (c == 20) { int len = 0; while(url_input_buffer[len]) len++; if (url_cursor < len) url_cursor++; } - else if (c == 127 || c == 8) { + else if (c == 19) { // LEFT if (url_cursor > 0) { - int len = 0; while(url_input_buffer[len]) len++; - for (int k=url_cursor-1; k= 32 && c <= 126 && url_cursor < 511) { - int len = 0; while(url_input_buffer[len]) len++; - for (int k=len; k>=url_cursor; k--) url_input_buffer[k+1] = url_input_buffer[k]; - url_input_buffer[url_cursor++] = c; + else if (c == 20) { // RIGHT + int len = (int)strlen(url_input_buffer); + if (url_cursor < len) { + const char *next = text_next_utf8(url_input_buffer + url_cursor); + url_cursor = (int)(next - url_input_buffer); + } + } + else if (c == 127 || c == 8) { + if (url_cursor > 0) { + int len = (int)strlen(url_input_buffer); + const char *prev = text_prev_utf8(url_input_buffer, url_input_buffer + url_cursor); + int char_len = (int)((url_input_buffer + url_cursor) - prev); + for (int k=url_cursor-char_len; k= 32 && cp != 127 && url_cursor < 511) { + char utf8[4]; + int clen = text_encode_utf8(cp, utf8); + int len = (int)strlen(url_input_buffer); + if (clen > 0 && len + clen < 511) { + for (int k=len; k>=url_cursor; k--) url_input_buffer[k+clen] = url_input_buffer[k]; + for (int k=0; kinput_cursor > 0) el->input_cursor--; } - else if (c == 20) { if (el->input_cursor < len) el->input_cursor++; } - else if (c == 127 || c == 8) { + else if (c == 19) { // LEFT if (el->input_cursor > 0) { - for (int k=el->input_cursor-1; kattr_value[k] = el->attr_value[k+1]; - el->input_cursor--; + const char *prev = text_prev_utf8(el->attr_value, el->attr_value + el->input_cursor); + el->input_cursor = (int)(prev - el->attr_value); } } - else if (c >= 32 && c <= 126 && len < 255) { - for (int k=len; k>=el->input_cursor; k--) el->attr_value[k+1] = el->attr_value[k]; - el->attr_value[el->input_cursor++] = c; + else if (c == 20) { // RIGHT + if (el->input_cursor < len) { + const char *next = text_next_utf8(el->attr_value + el->input_cursor); + el->input_cursor = (int)(next - el->attr_value); + } + } + else if (c == 127 || c == 8) { + if (el->input_cursor > 0) { + const char *prev = text_prev_utf8(el->attr_value, el->attr_value + el->input_cursor); + int char_len = (int)((el->attr_value + el->input_cursor) - prev); + for (int k=el->input_cursor-char_len; kattr_value[k] = el->attr_value[k+char_len]; + el->input_cursor -= char_len; + el->attr_value[len - char_len] = 0; + } + } + else if (cp >= 32 && cp != 127 && len < 255) { + char utf8[4]; + int clen = text_encode_utf8(cp, utf8); + if (clen > 0 && len + clen < 255) { + for (int k=len; k>=el->input_cursor; k--) el->attr_value[k+clen] = el->attr_value[k]; + for (int k=0; kattr_value[el->input_cursor + k] = utf8[k]; + el->input_cursor += clen; + } } int max_v = (el->w - 10) / 8; diff --git a/src/userland/gui/grapher.c b/src/userland/gui/grapher.c index 61bbda4..3002277 100644 --- a/src/userland/gui/grapher.c +++ b/src/userland/gui/grapher.c @@ -6,6 +6,7 @@ // BOREDOS_APP_ICONS: /Library/images/icons/colloid/se.sjoerd.Graphs.png;/Library/images/icons/colloid/app-icon-preview.png #include "syscall.h" #include "libui.h" +#include "utf-8.h" #include "../../wm/libwidget.h" #include #include "stdlib.h" @@ -1598,16 +1599,17 @@ int main(void) { } else if (ev.type == GUI_EVENT_PAINT) { needs_repaint = true; } else if (ev.type == GUI_EVENT_KEY) { + uint32_t cp = (uint32_t)ev.arg4; char c = (char)ev.arg1; if (tb_equation.focused) { - if (c == '\n') { - eq_len = strlen(eq_buffer); + if (cp == '\n') { + eq_len = (int)strlen(eq_buffer); parse_equation(); if (graph_mode == MODE_2D) autofit_2d_view(); needs_repaint = true; } else { - widget_textbox_handle_key(&tb_equation, c, NULL); - eq_len = strlen(eq_buffer); + widget_textbox_handle_key(&tb_equation, cp, (int)ev.arg1, NULL); + eq_len = (int)strlen(eq_buffer); needs_repaint = true; } } else if ((c == 'r' || c == 'R') && ev.arg3 == 1) { @@ -1699,7 +1701,7 @@ int main(void) { } } - widget_textbox_handle_mouse(&tb_equation, mx, my, true, NULL); + widget_textbox_handle_mouse(&wctx, &tb_equation, mx, my, true, NULL); needs_repaint = true; } else if (ev.type == GUI_EVENT_MOUSE_DOWN) { diff --git a/src/userland/gui/settings.c b/src/userland/gui/settings.c index c45b8ab..a34c4cd 100644 --- a/src/userland/gui/settings.c +++ b/src/userland/gui/settings.c @@ -10,6 +10,7 @@ #include #include #include "../../wm/libwidget.h" +#include "utf-8.h" #define COLOR_COFFEE 0xFF6B4423 #define COLOR_TEAL 0xFF008080 @@ -1069,13 +1070,13 @@ static void control_panel_handle_mouse(int x, int y, bool is_down, bool is_click } if (current_view == VIEW_WALLPAPER) { - if (widget_textbox_handle_mouse(&tb_r, x, y, is_click, NULL)) { + if (widget_textbox_handle_mouse(&settings_ctx, &tb_r, x, y, is_click, NULL)) { focused_field = 0; input_cursor = tb_r.cursor_pos; return; } - if (widget_textbox_handle_mouse(&tb_g, x, y, is_click, NULL)) { + if (widget_textbox_handle_mouse(&settings_ctx, &tb_g, x, y, is_click, NULL)) { focused_field = 1; input_cursor = tb_g.cursor_pos; return; } - if (widget_textbox_handle_mouse(&tb_b, x, y, is_click, NULL)) { + if (widget_textbox_handle_mouse(&settings_ctx, &tb_b, x, y, is_click, NULL)) { focused_field = 2; input_cursor = tb_b.cursor_pos; return; } @@ -1112,10 +1113,10 @@ static void control_panel_handle_mouse(int x, int y, bool is_down, bool is_click } if (current_view == VIEW_NETWORK) { - if (widget_textbox_handle_mouse(&tb_ip, x, y, is_click, NULL)) { + if (widget_textbox_handle_mouse(&settings_ctx, &tb_ip, x, y, is_click, NULL)) { focused_field = 5; input_cursor = tb_ip.cursor_pos; return; } - if (widget_textbox_handle_mouse(&tb_dns, x, y, is_click, NULL)) { + if (widget_textbox_handle_mouse(&settings_ctx, &tb_dns, x, y, is_click, NULL)) { focused_field = 6; input_cursor = tb_dns.cursor_pos; return; } @@ -1239,10 +1240,10 @@ static void control_panel_handle_mouse(int x, int y, bool is_down, bool is_click } if (current_view == VIEW_DISPLAY) { - if (widget_textbox_handle_mouse(&tb_custom_w, x, y, is_click, NULL)) { + if (widget_textbox_handle_mouse(&settings_ctx, &tb_custom_w, x, y, is_click, NULL)) { focused_field = 3; disp_sel_res = 5; input_cursor = tb_custom_w.cursor_pos; return; } - if (widget_textbox_handle_mouse(&tb_custom_h, x, y, is_click, NULL)) { + if (widget_textbox_handle_mouse(&settings_ctx, &tb_custom_h, x, y, is_click, NULL)) { focused_field = 4; disp_sel_res = 5; input_cursor = tb_custom_h.cursor_pos; return; } } @@ -1264,33 +1265,34 @@ static void control_panel_handle_mouse(int x, int y, bool is_down, bool is_click } } -static void control_panel_handle_key(char c, bool pressed) { +static void control_panel_handle_key(uint32_t codepoint, int legacy, bool pressed) { if (!pressed) return; if (focused_field < 0) return; if (current_view == VIEW_WALLPAPER) { - if (focused_field == 0) widget_textbox_handle_key(&tb_r, c, NULL); - if (focused_field == 1) widget_textbox_handle_key(&tb_g, c, NULL); - if (focused_field == 2) widget_textbox_handle_key(&tb_b, c, NULL); - if (c == '\t') focused_field = (focused_field + 1) % 3; + if (focused_field == 0) widget_textbox_handle_key(&tb_r, codepoint, legacy, NULL); + if (focused_field == 1) widget_textbox_handle_key(&tb_g, codepoint, legacy, NULL); + if (focused_field == 2) widget_textbox_handle_key(&tb_b, codepoint, legacy, NULL); + if (legacy == '\t') focused_field = (focused_field + 1) % 3; } else if (current_view == VIEW_DISPLAY) { char *focused_buffer = (focused_field == 3) ? custom_res_w : custom_res_h; - if (c == '\b') { + if (legacy == '\b' || legacy == 127) { if (input_cursor > 0) { - input_cursor--; + const char *prev = text_prev_utf8(focused_buffer, focused_buffer + input_cursor); + input_cursor = (int)(prev - focused_buffer); focused_buffer[input_cursor] = '\0'; } - } else if (c >= '0' && c <= '9') { + } else if (codepoint >= '0' && codepoint <= '9') { if (input_cursor < 5) { - focused_buffer[input_cursor] = c; + focused_buffer[input_cursor] = (char)codepoint; input_cursor++; focused_buffer[input_cursor] = '\0'; } } } else if (current_view == VIEW_NETWORK) { - if (focused_field == 5) widget_textbox_handle_key(&tb_ip, c, NULL); - if (focused_field == 6) widget_textbox_handle_key(&tb_dns, c, NULL); - if (c == '\t') focused_field = (focused_field == 5) ? 6 : 5; + if (focused_field == 5) widget_textbox_handle_key(&tb_ip, codepoint, legacy, NULL); + if (focused_field == 6) widget_textbox_handle_key(&tb_dns, codepoint, legacy, NULL); + if (legacy == '\t') focused_field = (focused_field == 5) ? 6 : 5; } } @@ -1434,11 +1436,11 @@ int main(int argc, char **argv) { } } else if (ev.type == GUI_EVENT_KEY) { - control_panel_handle_key((char)ev.arg1, true); + control_panel_handle_key((uint32_t)ev.arg4, (int)ev.arg1, true); dirty = true; } else if (ev.type == GUI_EVENT_KEYUP) { - control_panel_handle_key((char)ev.arg1, false); + control_panel_handle_key((uint32_t)ev.arg4, (int)ev.arg1, false); } else if (ev.type == GUI_EVENT_CLOSE) { sys_exit(0); diff --git a/src/userland/gui/terminal.c b/src/userland/gui/terminal.c index b096c63..0af8504 100644 --- a/src/userland/gui/terminal.c +++ b/src/userland/gui/terminal.c @@ -1140,10 +1140,10 @@ static void handle_key(gui_event_t *ev) { if (!ctrl) { if (legacy == KEY_BACKSPACE) { if (s->input_len > 0) { - // Find previous UTF-8 character boundary const char *prev = text_prev_utf8(s->current_input, s->current_input + s->input_len); s->input_len = (int)(prev - s->current_input); s->current_input[s->input_len] = 0; + s->unacknowledged_chars--; } } else if (codepoint >= 32 && codepoint != 127) { char utf8[4]; @@ -1161,8 +1161,6 @@ static void handle_key(gui_event_t *ev) { s->input_len = 0; s->current_input[0] = 0; s->unacknowledged_chars = 0; - } else if (legacy == KEY_BACKSPACE) { - s->unacknowledged_chars--; } update_input_color(s); diff --git a/src/wm/libwidget.c b/src/wm/libwidget.c index ecd824d..3f128c9 100644 --- a/src/wm/libwidget.c +++ b/src/wm/libwidget.c @@ -1,4 +1,5 @@ #include "libwidget.h" +#include "utf-8.h" #include #define COLOR_GRAY 0xFFC0C0C0 @@ -237,17 +238,14 @@ void widget_textbox_draw(widget_context_t *ctx, widget_textbox_t *tb) { if (ctx->draw_string && tb->text) { int max_w = tb->w - 15; - int scroll_x = 0; int text_w = 0; if (ctx->measure_string_width) { text_w = ctx->measure_string_width(ctx->user_data, tb->text); } else { - text_w = string_len(tb->text) * 8; + text_w = (int)text_strlen_utf8(tb->text) * 8; } - if (text_w > max_w) scroll_x = text_w - max_w; - // Very basic simple drawing, without proper clipping since context lacks it ctx->draw_string(ctx->user_data, tb->x + 5, tb->y + (tb->h - 8) / 2, tb->text, text_color); @@ -263,7 +261,13 @@ void widget_textbox_draw(widget_context_t *ctx, widget_textbox_t *tb) { tmp[k] = 0; cx = ctx->measure_string_width(ctx->user_data, tmp); } else { - cx = tb->cursor_pos * 8; + char tmp[256]; + int k = 0; + for (k = 0; k < tb->cursor_pos && tb->text[k]; k++) { + tmp[k] = tb->text[k]; + } + tmp[k] = 0; + cx = (int)text_strlen_utf8(tmp) * 8; } if (cx > max_w) cx = max_w; // clamped to visible end @@ -273,50 +277,90 @@ void widget_textbox_draw(widget_context_t *ctx, widget_textbox_t *tb) { } } -bool widget_textbox_handle_mouse(widget_textbox_t *tb, int mx, int my, bool mouse_clicked, void *user_data) { +bool widget_textbox_handle_mouse(widget_context_t *ctx, widget_textbox_t *tb, int mx, int my, bool mouse_clicked, void *user_data) { + (void)user_data; bool in_bounds = (mx >= tb->x && mx < tb->x + tb->w && my >= tb->y && my < tb->y + tb->h); if (mouse_clicked) { tb->focused = in_bounds; if (in_bounds && tb->text) { int rel_x = mx - (tb->x + 5); if (rel_x < 0) rel_x = 0; - // Rough estimation for fixed-width font 8px chars - int new_pos = rel_x / 8; - int len = string_len(tb->text); - if (new_pos > len) new_pos = len; - tb->cursor_pos = new_pos; + + int cur_x = 0; + int byte_pos = 0; + while (tb->text[byte_pos]) { + int adv; + text_decode_utf8(tb->text + byte_pos, &adv); + char buf[5]; + for (int k = 0; k < adv; k++) buf[k] = tb->text[byte_pos + k]; + buf[adv] = 0; + + int cw; + if (ctx && ctx->measure_string_width) { + cw = ctx->measure_string_width(ctx->user_data, buf); + } else { + cw = 8; + } + + if (rel_x < cur_x + cw / 2) { + tb->cursor_pos = byte_pos; + return true; + } + if (rel_x < cur_x + cw) { + tb->cursor_pos = byte_pos + adv; + return true; + } + + cur_x += cw; + byte_pos += adv; + } + tb->cursor_pos = byte_pos; } } return in_bounds; } -bool widget_textbox_handle_key(widget_textbox_t *tb, char c, void *user_data) { +bool widget_textbox_handle_key(widget_textbox_t *tb, uint32_t codepoint, int legacy, void *user_data) { if (!tb->focused || !tb->text) return false; - int len = string_len(tb->text); - if (c == 19) { // LEFT - if (tb->cursor_pos > 0) tb->cursor_pos--; + int len = (int)string_len(tb->text); + if (legacy == 19) { // LEFT + if (tb->cursor_pos > 0) { + const char *prev = text_prev_utf8(tb->text, tb->text + tb->cursor_pos); + tb->cursor_pos = (int)(prev - tb->text); + } return true; - } else if (c == 20) { // RIGHT - if (tb->cursor_pos < len) tb->cursor_pos++; + } else if (legacy == 20) { // RIGHT + if (tb->cursor_pos < len) { + const char *next = text_next_utf8(tb->text + tb->cursor_pos); + tb->cursor_pos = (int)(next - tb->text); + } return true; } - if (c == '\b') { // backspace + if (legacy == '\b' || legacy == 127) { // backspace if (tb->cursor_pos > 0) { - for (int i = tb->cursor_pos - 1; i < len; i++) { - tb->text[i] = tb->text[i + 1]; + const char *prev = text_prev_utf8(tb->text, tb->text + tb->cursor_pos); + int char_len = (int)((tb->text + tb->cursor_pos) - prev); + + for (int i = tb->cursor_pos - char_len; i < len - char_len; i++) { + tb->text[i] = tb->text[i + char_len]; } - tb->cursor_pos--; + tb->cursor_pos -= char_len; + tb->text[len - char_len] = 0; if (tb->on_change) tb->on_change(user_data); } - } else if (c >= 32 && c < 127) { - if (len < tb->max_len - 1) { + } else if (codepoint >= 32 && codepoint != 127) { + char utf8[4]; + int clen = text_encode_utf8(codepoint, utf8); + if (clen > 0 && len + clen < tb->max_len) { for (int i = len; i >= tb->cursor_pos; i--) { - tb->text[i + 1] = tb->text[i]; + tb->text[i + clen] = tb->text[i]; } - tb->text[tb->cursor_pos] = c; - tb->cursor_pos++; + for (int i = 0; i < clen; i++) { + tb->text[tb->cursor_pos + i] = utf8[i]; + } + tb->cursor_pos += clen; if (tb->on_change) tb->on_change(user_data); } } diff --git a/src/wm/libwidget.h b/src/wm/libwidget.h index cfd3697..a0c3d25 100644 --- a/src/wm/libwidget.h +++ b/src/wm/libwidget.h @@ -60,8 +60,8 @@ typedef struct { void widget_textbox_init(widget_textbox_t *tb, int x, int y, int w, int h, char *buffer, int max_len); void widget_textbox_draw(widget_context_t *ctx, widget_textbox_t *tb); -bool widget_textbox_handle_mouse(widget_textbox_t *tb, int mx, int my, bool mouse_clicked, void *user_data); -bool widget_textbox_handle_key(widget_textbox_t *tb, char c, void *user_data); +bool widget_textbox_handle_mouse(widget_context_t *ctx, widget_textbox_t *tb, int mx, int my, bool mouse_clicked, void *user_data); +bool widget_textbox_handle_key(widget_textbox_t *tb, uint32_t codepoint, int legacy, void *user_data); // --- Dropdown --- typedef struct { diff --git a/src/wm/utf-8.c b/src/wm/utf-8.c new file mode 100644 index 0000000..edc5af6 --- /dev/null +++ b/src/wm/utf-8.c @@ -0,0 +1,115 @@ +#include "utf-8.h" + +static int utf8_write_replacement(char *out) { + out[0] = (char)0xEF; + out[1] = (char)0xBF; + out[2] = (char)0xBD; + return 3; +} + +uint32_t text_decode_utf8(const char *s, int *advance) { + const unsigned char *u = (const unsigned char *)s; + + if (!u || u[0] == 0) { + if (advance) *advance = 0; + return 0; + } + + if ((u[0] & 0x80) == 0) { + if (advance) *advance = 1; + return u[0]; + } + + if ((u[0] & 0xE0) == 0xC0 && + (u[1] & 0xC0) == 0x80) { + if (advance) *advance = 2; + return ((u[0] & 0x1F) << 6) | + (u[1] & 0x3F); + } + + if ((u[0] & 0xF0) == 0xE0 && + (u[1] & 0xC0) == 0x80 && + (u[2] & 0xC0) == 0x80) { + if (advance) *advance = 3; + return ((u[0] & 0x0F) << 12) | + ((u[1] & 0x3F) << 6) | + (u[2] & 0x3F); + } + + if ((u[0] & 0xF8) == 0xF0 && + (u[1] & 0xC0) == 0x80 && + (u[2] & 0xC0) == 0x80 && + (u[3] & 0xC0) == 0x80) { + if (advance) *advance = 4; + return ((u[0] & 0x07) << 18) | + ((u[1] & 0x3F) << 12) | + ((u[2] & 0x3F) << 6) | + (u[3] & 0x3F); + } + + if (advance) *advance = 1; + return 0xFFFD; +} + +int text_encode_utf8(uint32_t cp, char *out) { + if (cp <= 0x7F) { + out[0] = (char)cp; + return 1; + } + + if (cp <= 0x7FF) { + out[0] = 0xC0 | (cp >> 6); + out[1] = 0x80 | (cp & 0x3F); + return 2; + } + + if (cp <= 0xFFFF) { + out[0] = 0xE0 | (cp >> 12); + out[1] = 0x80 | ((cp >> 6) & 0x3F); + out[2] = 0x80 | (cp & 0x3F); + return 3; + } + + if (cp <= 0x10FFFF) { + out[0] = 0xF0 | (cp >> 18); + out[1] = 0x80 | ((cp >> 12) & 0x3F); + out[2] = 0x80 | ((cp >> 6) & 0x3F); + out[3] = 0x80 | (cp & 0x3F); + return 4; + } + + return utf8_write_replacement(out); +} + +const char* text_next_utf8(const char *s) { + if (!s || *s == 0) return s; + + int adv; + text_decode_utf8(s, &adv); + return s + adv; +} + +const char* text_prev_utf8(const char *start, const char *s) { + if (!s || s <= start) return start; + + s--; + while (s > start && ((*s & 0xC0) == 0x80)) { + s--; + } + return s; +} + +int text_strlen_utf8(const char *s) { + if (!s) return 0; + + int count = 0; + int adv; + + while (*s) { + text_decode_utf8(s, &adv); + s += adv; + count++; + } + + return count; +} \ No newline at end of file diff --git a/src/wm/utf-8.h b/src/wm/utf-8.h new file mode 100644 index 0000000..1af7b36 --- /dev/null +++ b/src/wm/utf-8.h @@ -0,0 +1,25 @@ +#ifndef UTF_8_H +#define UTF_8_H + +#include + +// Decode one UTF-8 codepoint +// s: input string +// advance: number of bytes consumed +uint32_t text_decode_utf8(const char *s, int *advance); + +// Encode one codepoint into UTF-8 +// out must be at least 4 bytes +// return: number of bytes written +int text_encode_utf8(uint32_t cp, char *out); + +// Move to next UTF-8 character +const char* text_next_utf8(const char *s); + +// Move to previous UTF-8 character +const char* text_prev_utf8(const char *start, const char *s); + +// Count characters (not bytes) +int text_strlen_utf8(const char *s); + +#endif \ No newline at end of file