joystick UI: proper accents, emojis stripped, new unlock mechanism

This commit is contained in:
liquidraver
2026-06-11 20:55:56 +02:00
parent cc8ade8298
commit d4a48ae5f3
5 changed files with 262 additions and 73 deletions
+44 -14
View File
@@ -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;
};
@@ -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;
+86 -34
View File
@@ -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 === */
+105 -16
View File
@@ -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);
}
}
+13 -3
View File
@@ -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);