From d4a48ae5f33c37d29f1e54f6ab80bfe0ccd1b86f Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Thu, 11 Jun 2026 20:55:56 +0200 Subject: [PATCH] joystick UI: proper accents, emojis stripped, new unlock mechanism --- .../helpers/ui-joystick/joystick_display.h | 58 +++++++-- .../helpers/ui-joystick/joystick_ui_task.cpp | 20 ++- zephcore/helpers/ui/cfb_font_0608.c | 120 ++++++++++++----- zephcore/helpers/ui/display.c | 121 +++++++++++++++--- zephcore/helpers/ui/display.h | 16 ++- 5 files changed, 262 insertions(+), 73 deletions(-) diff --git a/zephcore/helpers/ui-joystick/joystick_display.h b/zephcore/helpers/ui-joystick/joystick_display.h index 66f53ed..569d27d 100644 --- a/zephcore/helpers/ui-joystick/joystick_display.h +++ b/zephcore/helpers/ui-joystick/joystick_display.h @@ -47,19 +47,20 @@ public: int fontW() const { int w = mc_display_font_width(); return w ? w : 6; } int fontH() const { int h = mc_display_font_height(); return h ? h : 8; } int getTextWidth(const char *text) const { - return text ? (int)strlen(text) * fontW() : 0; + if (!text) return 0; + char buf[SANITIZE_BUF]; + return (int)strlen(sanitized(buf, sizeof(buf), text)) * fontW(); } - /* ===== Text drawing ===== */ + /* ===== Text drawing ===== + * All entry points transcode UTF-8 to the display charset first + * (utf8_to_display: Latin-1/Latin-2 mapped, emoji stripped), so screens + * can pass contact/channel names straight through. Measurement uses the + * same transcoding, keeping widths in glyphs rather than UTF-8 bytes. */ void drawTextLeftAlign(int x, int y, const char *text) { if (!text) return; - if (_color == YELLOW) { - int w = getTextWidth(text); - mc_display_fill_rect(x, y, w, fontH()); - mc_display_text(x, y, text, true); - } else { - mc_display_text(x, y, text, _color == DARK); - } + char buf[SANITIZE_BUF]; + drawSanitized(x, y, sanitized(buf, sizeof(buf), text)); } void drawTextRightAlign(int right_x, int y, const char *text) { @@ -85,19 +86,21 @@ public: int avail_chars = max_w / fw; if (avail_chars <= 0) return; - int len = (int)strlen(text); + char sbuf[SANITIZE_BUF]; + const char *s = sanitized(sbuf, sizeof(sbuf), text); + int len = (int)strlen(s); if (len <= avail_chars) { - drawTextLeftAlign(x, y, text); + drawSanitized(x, y, s); } else { /* Truncate with "…" (3 dots, but we only have ASCII) */ int truncate_at = avail_chars - 3; if (truncate_at < 0) truncate_at = 0; char buf[128]; int n = truncate_at < 127 ? truncate_at : 127; - memcpy(buf, text, n); + memcpy(buf, s, n); buf[n] = '.'; buf[n+1] = '.'; buf[n+2] = '.'; buf[n+3] = '\0'; - drawTextLeftAlign(x, y, buf); + drawSanitized(x, y, buf); } } @@ -145,12 +148,39 @@ public: void turnOn() { mc_display_on(); } void turnOff() { mc_display_off(); } - /* UTF8 utility */ + /* UTF8 utility — explicit pre-transcoding for screens that slice text + * afterwards (marquee windows etc.), so byte == glyph in their math. + * Re-sanitizing the result at draw time is a harmless pass-through. */ void translateUTF8ToBlocks(char *dst, const char *src, int size) { utf8_to_latin1(dst, src, (size_t)size); } private: + static constexpr size_t SANITIZE_BUF = 160; /* > any display line */ + + /* Returns text itself when pure ASCII (no copy), else the transcoded + * copy in buf. */ + static const char *sanitized(char *buf, size_t n, const char *text) { + for (const char *p = text; *p; p++) { + if ((uint8_t)*p >= 0x80) { + utf8_to_display(buf, text, n); + return buf; + } + } + return text; + } + + /* Color/invert draw logic on already-transcoded text. */ + void drawSanitized(int x, int y, const char *text) { + if (_color == YELLOW) { + int w = (int)strlen(text) * fontW(); + mc_display_fill_rect(x, y, w, fontH()); + mc_display_text(x, y, text, true); + } else { + mc_display_text(x, y, text, _color == DARK); + } + } + Color _color; int _cx, _cy; }; diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp index 58334e6..034e564 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp @@ -657,22 +657,30 @@ void JoystickUITask::renderAlertOverlay() void JoystickUITask::handleLockInput(char key, uint32_t now) { + /* Unlock sequence: LEFT, OK, RIGHT — dedicated GPIO keys delivered + * instantly. The back button is unusable here: its taps pass through + * the longpress + multi-tap devicetree filters, so a single tap is + * deferred 400 ms (tap-count window), a >=1 s hold becomes KEY_POWER, + * and a quick re-tap becomes KEY_B — which made the old Back-OK-Back + * sequence timing-sensitive. A wrong key restarts the sequence, except + * LEFT, which always (re)arms step 1. */ + uint8_t restart = (key == KEY_LEFT) ? 1 : 0; + switch (_lock_step) { case 0: - if (key == KEY_CANCEL) _lock_step = 1; - else _lock_step = 0; + _lock_step = restart; break; case 1: if (key == KEY_ENTER || key == KEY_ENTER_LONG) _lock_step = 2; - else _lock_step = 0; + else _lock_step = restart; break; case 2: - if (key == KEY_CANCEL) { + if (key == KEY_RIGHT) { _locked = false; _lock_step = 0; scheduleLockTimer(); } else { - _lock_step = 0; + _lock_step = restart; } break; default: @@ -723,7 +731,7 @@ void JoystickUITask::renderLockOverlay() mc_display_text((w - unread_w) / 2, 6 + fh + 6 + fh + 2, unread_buf, false); /* Three step unlock sequence */ - const char *toks[3] = { "Back", "OK", "Back" }; + const char *toks[3] = { "Left", "OK", "Right" }; const int gap = 6; int total_w = 0; for (int i = 0; i < 3; i++) total_w += (int)strlen(toks[i]) * fw; diff --git a/zephcore/helpers/ui/cfb_font_0608.c b/zephcore/helpers/ui/cfb_font_0608.c index 2033c6b..869ecc1 100644 --- a/zephcore/helpers/ui/cfb_font_0608.c +++ b/zephcore/helpers/ui/cfb_font_0608.c @@ -203,43 +203,95 @@ static const uint8_t cfb_font_0608[224][6] = { /* 126 (~) */ { 0x08, 0x08, 0x2A, 0x1C, 0x08, 0x00 }, - /* === Code points 127-159 (DEL + C1 control codes) === */ + /* === Code points 127-159 (DEL + Latin-2 extras) === + * + * 128-159 host the Latin Extended-A letters needed for Central European + * names (Hungarian, Czech, Slovak, Polish, Croatian). utf8_to_display() + * in display.c maps the Unicode code points into these slots. + * + * Same construction rules as the Latin-1 blocks below: + * uppercase: base letter shifted down 1 px (<<1), accent on row 0 + * acute col3 |= 0x01 + * caron col1,col3 |= 0x01 (col2 alone is dot-above for Z; + * side effect: E caron renders like E diaeresis) + * double acute col2,col4 |= 0x01 (vs diaeresis col1,col3) + * ring col1,col2,col3 |= 0x01 + * ogonek unshifted base, col4 |= 0x80 (hook on row 7) + * lowercase: rows 0-1 free, no shift needed + * acute col2 |= 0x01, col3 |= 0x02 + * caron col1 |= 0x01, col2 |= 0x02, col3 |= 0x01 (v shape) + * double acute col1 |= 0x01, col2 |= 0x02, col3 |= 0x01, col4 |= 0x02 + * dot above col2 |= 0x01 + * ogonek hook below: col3/col4 |= 0x80 + */ /* 127 (DEL) - blank */ { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, - /* 128-159: C1 control codes - blank */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 128 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 129 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 130 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 131 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 132 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 133 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 134 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 135 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 136 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 137 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 138 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 139 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 140 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 141 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 142 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 143 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 144 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 145 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 146 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 147 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 148 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 149 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 150 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 151 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 152 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 153 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 154 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 155 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 156 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 157 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 158 */ - { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, /* 159 */ + + /* 128 O double acute (U+0150) */ + { 0x7C, 0x82, 0x83, 0x82, 0x7D, 0x00 }, + /* 129 o double acute (U+0151) */ + { 0x38, 0x45, 0x46, 0x45, 0x3A, 0x00 }, + /* 130 U double acute (U+0170) */ + { 0x7E, 0x80, 0x81, 0x80, 0x7F, 0x00 }, + /* 131 u double acute (U+0171) */ + { 0x3C, 0x41, 0x42, 0x21, 0x7E, 0x00 }, + /* 132 A ogonek (U+0104) */ + { 0x7E, 0x11, 0x11, 0x11, 0xFE, 0x00 }, + /* 133 a ogonek (U+0105) */ + { 0x20, 0x54, 0x54, 0x54, 0xF8, 0x00 }, + /* 134 C acute (U+0106) */ + { 0x7C, 0x82, 0x82, 0x83, 0x44, 0x00 }, + /* 135 c acute (U+0107) */ + { 0x38, 0x44, 0x45, 0x46, 0x20, 0x00 }, + /* 136 C caron (U+010C) */ + { 0x7C, 0x83, 0x82, 0x83, 0x44, 0x00 }, + /* 137 c caron (U+010D) */ + { 0x38, 0x45, 0x46, 0x45, 0x20, 0x00 }, + /* 138 E caron (U+011A) */ + { 0xFE, 0x93, 0x92, 0x93, 0x82, 0x00 }, + /* 139 e caron (U+011B) */ + { 0x38, 0x55, 0x56, 0x55, 0x18, 0x00 }, + /* 140 E ogonek (U+0118) */ + { 0x7F, 0x49, 0x49, 0x49, 0xC1, 0x00 }, + /* 141 e ogonek (U+0119) */ + { 0x38, 0x54, 0x54, 0xD4, 0x18, 0x00 }, + /* 142 L stroke (U+0141) */ + { 0x7F, 0x48, 0x44, 0x40, 0x40, 0x00 }, + /* 143 l stroke (U+0142) */ + { 0x00, 0x51, 0x7F, 0x44, 0x00, 0x00 }, + /* 144 N acute (U+0143) */ + { 0xFE, 0x08, 0x10, 0x21, 0xFE, 0x00 }, + /* 145 n acute (U+0144) */ + { 0x7C, 0x08, 0x05, 0x06, 0x78, 0x00 }, + /* 146 R caron (U+0158) */ + { 0xFE, 0x13, 0x32, 0x53, 0x8C, 0x00 }, + /* 147 r caron (U+0159) */ + { 0x7C, 0x09, 0x06, 0x05, 0x08, 0x00 }, + /* 148 S acute (U+015A) */ + { 0x8C, 0x92, 0x92, 0x93, 0x62, 0x00 }, + /* 149 s acute (U+015B) */ + { 0x48, 0x54, 0x55, 0x56, 0x20, 0x00 }, + /* 150 S caron (U+0160) */ + { 0x8C, 0x93, 0x92, 0x93, 0x62, 0x00 }, + /* 151 s caron (U+0161) */ + { 0x48, 0x55, 0x56, 0x55, 0x20, 0x00 }, + /* 152 U ring (U+016E) */ + { 0x7E, 0x81, 0x81, 0x81, 0x7E, 0x00 }, + /* 153 u ring (U+016F) */ + { 0x3C, 0x42, 0x41, 0x22, 0x7C, 0x00 }, + /* 154 Z acute (U+0179) */ + { 0xC2, 0xA2, 0x92, 0x8B, 0x86, 0x00 }, + /* 155 z acute (U+017A) */ + { 0x44, 0x64, 0x55, 0x4E, 0x44, 0x00 }, + /* 156 Z dot above (U+017B) */ + { 0xC2, 0xA2, 0x93, 0x8A, 0x86, 0x00 }, + /* 157 z dot above (U+017C) */ + { 0x44, 0x64, 0x55, 0x4C, 0x44, 0x00 }, + /* 158 Z caron (U+017D) */ + { 0xC2, 0xA3, 0x92, 0x8B, 0x86, 0x00 }, + /* 159 z caron (U+017E) */ + { 0x44, 0x65, 0x56, 0x4D, 0x44, 0x00 }, /* === Latin-1 Supplement 160-255 === */ diff --git a/zephcore/helpers/ui/display.c b/zephcore/helpers/ui/display.c index 0709604..7efadee 100644 --- a/zephcore/helpers/ui/display.c +++ b/zephcore/helpers/ui/display.c @@ -565,45 +565,134 @@ const struct device *mc_display_get_device(void) return disp_initialized ? disp_dev : NULL; } -/* ========== UTF-8 to Latin-1 Sanitizer ========== */ -void utf8_to_latin1(char *dst, const char *src, size_t dst_size) +/* ========== UTF-8 to display-charset sanitizer ========== + * + * The 6x8 font covers Latin-1 at its native code points (0xA0-0xFF) and + * hosts 32 Latin Extended-A letters (Hungarian, Czech, Slovak, Polish, ...) + * in the otherwise-unused C1 range 128-159 (see cfb_font_0608.c). */ + +/* Latin Extended-A code points with a real glyph -> font slot 128-159 */ +static const struct { + uint16_t cp; + uint8_t slot; +} latin2_slots[] = { + { 0x0104, 132 }, { 0x0105, 133 }, /* A/a ogonek */ + { 0x0106, 134 }, { 0x0107, 135 }, /* C/c acute */ + { 0x010C, 136 }, { 0x010D, 137 }, /* C/c caron */ + { 0x0118, 140 }, { 0x0119, 141 }, /* E/e ogonek */ + { 0x011A, 138 }, { 0x011B, 139 }, /* E/e caron */ + { 0x0141, 142 }, { 0x0142, 143 }, /* L/l stroke */ + { 0x0143, 144 }, { 0x0144, 145 }, /* N/n acute */ + { 0x0150, 128 }, { 0x0151, 129 }, /* O/o double acute */ + { 0x0158, 146 }, { 0x0159, 147 }, /* R/r caron */ + { 0x015A, 148 }, { 0x015B, 149 }, /* S/s acute */ + { 0x0160, 150 }, { 0x0161, 151 }, /* S/s caron */ + { 0x016E, 152 }, { 0x016F, 153 }, /* U/u ring */ + { 0x0170, 130 }, { 0x0171, 131 }, /* U/u double acute */ + { 0x0179, 154 }, { 0x017A, 155 }, /* Z/z acute */ + { 0x017B, 156 }, { 0x017C, 157 }, /* Z/z dot above */ + { 0x017D, 158 }, { 0x017E, 159 }, /* Z/z caron */ +}; + +/* Base-letter fold for all of Latin Extended-A (U+0100..U+017F), indexed by + * cp - 0x100. Used for code points without a glyph slot above. */ +static const char latin_ext_a_fold[] = + "AaAaAa" "CcCcCcCc" "DdDd" "EeEeEeEeEe" "GgGgGgGg" "HhHh" + "IiIiIiIiIi" "Ii" "Jj" "Kkk" "LlLlLlLlLl" "NnNnNnnNn" + "OoOoOoOo" "RrRrRr" "SsSsSsSs" "TtTtTt" "UuUuUuUuUuUu" + "Ww" "YyY" "ZzZzZz" "s"; + +static uint8_t display_charset_map(uint32_t cp) +{ + if (cp >= 0xA0 && cp <= 0xFF) { + return (uint8_t)cp; /* native Latin-1 glyph */ + } + if (cp >= 0x100 && cp <= 0x17F) { + for (size_t i = 0; i < ARRAY_SIZE(latin2_slots); i++) { + if (latin2_slots[i].cp == cp) { + return latin2_slots[i].slot; + } + } + return (uint8_t)latin_ext_a_fold[cp - 0x100]; + } + switch (cp) { /* Romanian uses comma-below variants */ + case 0x218: return 'S'; + case 0x219: return 's'; + case 0x21A: return 'T'; + case 0x21B: return 't'; + default: return 0; /* drop: emoji, other scripts, ... */ + } +} + +void utf8_to_display(char *dst, const char *src, size_t dst_size) { size_t di = 0; size_t si = 0; + if (dst_size == 0) { + return; + } while (src[si] && di < dst_size - 1) { uint8_t c = (uint8_t)src[si]; + uint32_t cp; + int len; if (c < 0x80) { dst[di++] = (char)c; si++; + continue; } else if ((c & 0xE0) == 0xC0) { - uint8_t c2 = (uint8_t)src[si + 1]; - - if ((c2 & 0xC0) != 0x80) { - si++; - continue; - } - uint16_t cp = ((c & 0x1F) << 6) | (c2 & 0x3F); - if (cp >= 0xA0 && cp <= 0xFF) { - dst[di++] = (char)(uint8_t)cp; - } - si += 2; + cp = c & 0x1F; + len = 2; } else if ((c & 0xF0) == 0xE0) { - si += 3; + cp = c & 0x0F; + len = 3; } else if ((c & 0xF8) == 0xF0) { - si += 4; + cp = c & 0x07; + len = 4; } else { + /* Not a UTF-8 lead byte: already display-encoded text + * (or junk) — pass through so a second sanitizing pass + * is harmless. */ + dst[di++] = (char)c; si++; + continue; + } + + bool valid = true; + for (int k = 1; k < len; k++) { + if (((uint8_t)src[si + k] & 0xC0) != 0x80) { + valid = false; + break; + } + cp = (cp << 6) | ((uint8_t)src[si + k] & 0x3F); + } + if (!valid) { + dst[di++] = (char)c; /* lone lead byte: pass through */ + si++; + continue; + } + si += len; + + uint8_t out = display_charset_map(cp); + if (out) { + dst[di++] = (char)out; } } dst[di] = '\0'; +} + +/* Legacy entry point: transcode + trim leading spaces (names are sometimes + * space-padded to game sort order). */ +void utf8_to_latin1(char *dst, const char *src, size_t dst_size) +{ + utf8_to_display(dst, src, dst_size); size_t start = 0; while (dst[start] == ' ') { start++; } if (start > 0) { - memmove(dst, dst + start, di - start + 1); + memmove(dst, dst + start, strlen(dst + start) + 1); } } diff --git a/zephcore/helpers/ui/display.h b/zephcore/helpers/ui/display.h index b623a29..13cd622 100644 --- a/zephcore/helpers/ui/display.h +++ b/zephcore/helpers/ui/display.h @@ -174,9 +174,19 @@ void mc_display_epd_full_reset(void); const struct device *mc_display_get_device(void); /** - * Convert UTF-8 text to Latin-1 for display rendering. - * Passes ASCII unchanged, converts 2-byte Latin-1 (U+00A0-U+00FF), - * strips everything else (emojis, CJK). Trims leading spaces left by stripped chars. + * Convert UTF-8 text to the display charset for rendering. + * Passes ASCII unchanged, converts Latin-1 (U+00A0-U+00FF) to its native + * code points, maps 32 Latin-2 letters (Hungarian/Czech/Slovak/Polish/...) + * into font slots 128-159, folds the rest of Latin Extended-A to base ASCII + * letters, and strips everything else (emojis, CJK). Bytes that are not + * valid UTF-8 pass through unchanged, so already-converted text survives a + * second pass. + */ +void utf8_to_display(char *dst, const char *src, size_t dst_size); + +/** + * utf8_to_display() + leading-space trim (names are sometimes space-padded + * to game sort order). */ void utf8_to_latin1(char *dst, const char *src, size_t dst_size);