diff --git a/examples/companion_radio/ui-new/BotScreen.h b/examples/companion_radio/ui-new/BotScreen.h index cf0b0ec8..f22995b7 100644 --- a/examples/companion_radio/ui-new/BotScreen.h +++ b/examples/companion_radio/ui-new/BotScreen.h @@ -8,8 +8,6 @@ class BotScreen : public UIScreen { // Items: 0=Enable(DM), 1=Channel, 2=Trigger, 3=Reply DM, 4=Reply Ch static const int ITEM_COUNT = 5; - static const int ITEM_H = 11; - static const int START_Y = 12; static const int VAL_X = 70; int _sel; @@ -61,12 +59,15 @@ public: display.drawTextCentered(display.width() / 2, 0, "AUTO-REPLY BOT"); display.fillRect(0, 10, display.width(), 1); + const int lineH = display.getLineHeight(); + const int itemH = lineH + 1; + const int startY = 12; static const char* labels[] = { "Enable", "Channel", "Trigger", "Reply DM", "Reply Ch" }; for (int i = 0; i < ITEM_COUNT; i++) { - int y = START_Y + i * ITEM_H; + int y = startY + i * itemH; bool sel = (i == _sel); if (sel) { - display.fillRect(0, y - 1, display.width(), ITEM_H); + display.fillRect(0, y - 1, display.width(), itemH); display.setColor(DisplayDriver::DARK); } display.setCursor(2, y); diff --git a/examples/companion_radio/ui-new/CharTestScreen.h b/examples/companion_radio/ui-new/CharTestScreen.h index e12397f9..72d2abbf 100644 --- a/examples/companion_radio/ui-new/CharTestScreen.h +++ b/examples/companion_radio/ui-new/CharTestScreen.h @@ -38,7 +38,7 @@ public: display.print("^"); } if (_scroll + _visible < LINE_COUNT) { - display.setCursor(display.width() - 6, display.height() - lineH); + display.setCursor(display.width() - 6, startY + (_visible - 1) * lineH); display.print("v"); } return 500; @@ -64,6 +64,6 @@ const CharTestScreen::TestLine CharTestScreen::LINES[CharTestScreen::LINE_COUNT] { "HR", "čćđšžČĆĐŠŽ" }, // Croatian { "TR", "çğışöüİŞĞ" }, // Turkish { "LT", "āēīūģķļņŗėįų" }, // Baltic (Latvian/Lithuanian) - { "RU", "АБВГДЕЖЗабвгдеж" }, // Cyrillic (→ blocks █) - { "GR", "\xCE\xB1\xCE\xB2\xCE\xB3\xCE\xB4\xCE\xB5\xCE\xB6\xCE\xB7\xCE\xB8\xCE\xB9\xCE\xBA" }, // Greek αβγδεζηθικ (→ blocks █) + { "RU", "АБВГДЕЖЗабвгдеж" }, // Cyrillic + { "GR", "\xCE\xB1\xCE\xB2\xCE\xB3\xCE\xB4\xCE\xB5\xCE\xB6\xCE\xB7\xCE\xB8\xCE\xB9\xCE\xBA" }, // Greek αβγδεζηθικ }; diff --git a/examples/companion_radio/ui-new/FullscreenMsgView.h b/examples/companion_radio/ui-new/FullscreenMsgView.h index 0e7335c2..dde79b08 100644 --- a/examples/companion_radio/ui-new/FullscreenMsgView.h +++ b/examples/companion_radio/ui-new/FullscreenMsgView.h @@ -3,7 +3,7 @@ #include #include -static const int FS_CHARS_MAX = 32; // max buffer per line (larger than any font needs) +static const int FS_CHARS_MAX = 80; // max bytes per line; sized for worst-case narrow glyphs (122px / 2px = 61 chars) static const int FS_START_Y = 12; // header height (fixed, not font-dependent) struct FullscreenMsgView { @@ -16,24 +16,55 @@ struct FullscreenMsgView { void begin() { scroll = 0; active = true; } - static int wrapLines(const char* text, int chars, char out[][FS_CHARS_MAX], int max_lines) { + // Pixel-accurate word-wrap using display.getTextWidth(). + // Breaks at the last space that fits within max_px; hard-breaks long words. + static int wrapLines(DisplayDriver& display, const char* text, int max_px, + char out[][FS_CHARS_MAX], int max_lines) { int count = 0; - const char* p = text; - while (*p && count < max_lines) { - int len = strlen(p); - if (len <= chars) { - strncpy(out[count++], p, FS_CHARS_MAX - 1); - out[count - 1][len < FS_CHARS_MAX ? len : FS_CHARS_MAX - 1] = '\0'; - break; + const char* seg = text; + while (*seg && count < max_lines) { + const char* p = seg; + const char* last_sp = nullptr; // pointer to last space that fits + const char* last_fit = seg; // pointer past last char that fits + + while (*p && (p - seg) < FS_CHARS_MAX - 1) { + // Byte length of this UTF-8 codepoint + uint8_t b0 = (uint8_t)*p; + int cb = (b0 < 0x80) ? 1 : (b0 < 0xE0) ? 2 : (b0 < 0xF0) ? 3 : 4; + if ((p - seg) + cb >= FS_CHARS_MAX) break; + + // Measure [seg .. p+cb) + char buf[FS_CHARS_MAX]; + int n = (int)(p - seg) + cb; + memcpy(buf, seg, n); buf[n] = '\0'; + + // Stop if this character would overflow — but always accept the first one + // so we never get stuck on a single very-wide character. + if (display.getTextWidth(buf) > max_px && p > seg) break; + + if (*p == ' ') last_sp = p; + p += cb; + last_fit = p; } - int brk = chars; - for (int i = chars - 1; i > 0; i--) { - if (p[i] == ' ') { brk = i; break; } + + // Determine break point: prefer last space, fall back to last fitting char + const char* end; + const char* next_seg; + if (!*p) { + end = p; next_seg = p; // reached end of text + } else if (last_sp && last_sp > seg) { + end = last_sp; next_seg = last_sp + 1; // break at space + } else { + end = last_fit; next_seg = last_fit; // hard break (no space found) } - int copy = brk < FS_CHARS_MAX - 1 ? brk : FS_CHARS_MAX - 1; - strncpy(out[count], p, copy); - out[count++][copy] = '\0'; - p += brk + (p[brk] == ' ' ? 1 : 0); + + int len = (int)(end - seg); + if (len <= 0) { seg = *seg ? seg + 1 : seg; continue; } // safety + if (len > FS_CHARS_MAX - 1) len = FS_CHARS_MAX - 1; + memcpy(out[count], seg, len); + out[count][len] = '\0'; + count++; + seg = next_seg; } return count; } @@ -42,7 +73,7 @@ struct FullscreenMsgView { bool has_prev, bool has_next) { display.setTextSize(1); const int lineH = display.getLineHeight(); - const int chars = (display.width() - 6) / display.getCharWidth(); + const int max_px = display.width() - 6; const int visible = (display.height() - FS_START_Y - lineH) / lineH; display.setColor(DisplayDriver::LIGHT); @@ -54,7 +85,7 @@ struct FullscreenMsgView { char trans_text[512]; display.translateUTF8ToBlocks(trans_text, text, sizeof(trans_text)); char lines[12][FS_CHARS_MAX]; - int lcount = wrapLines(trans_text, chars, lines, 12); + int lcount = wrapLines(display, trans_text, max_px, lines, 12); int max_scroll = lcount > visible ? lcount - visible : 0; if (scroll > max_scroll) scroll = max_scroll; diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index ecd59b83..8f338243 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -50,10 +50,9 @@ class SettingsScreen : public UIScreen { Count }; - static const int VISIBLE_ITEMS = 4; - static const int ITEM_H = 11; - static const int START_Y = 12; static const int VAL_X = 80; + int _vis; // visible item rows — updated each render() from getLineHeight() + int _item_h; // item row height in pixels — updated each render() int _selected; int _scroll; @@ -223,7 +222,7 @@ class SettingsScreen : public UIScreen { if (sel) { display.setColor(DisplayDriver::LIGHT); - display.fillRect(0, y - 1, display.width(), ITEM_H); + display.fillRect(0, y - 1, display.width(), _item_h); display.setColor(DisplayDriver::DARK); } else { display.setColor(DisplayDriver::LIGHT); @@ -335,7 +334,8 @@ class SettingsScreen : public UIScreen { public: SettingsScreen(UITask* task) - : _task(task), _selected(BRIGHTNESS), _scroll(0), _dirty(false), _edit_slot(-1) {} + : _task(task), _selected(BRIGHTNESS), _scroll(0), _dirty(false), _edit_slot(-1), + _vis(4), _item_h(11) {} void markClean() { _dirty = false; } @@ -351,19 +351,23 @@ public: display.drawTextCentered(display.width() / 2, 0, "SETTINGS"); display.fillRect(0, 10, display.width(), 1); - for (int i = 0; i < VISIBLE_ITEMS && (_scroll + i) < SettingItem::Count; i++) { - renderItem(display, _scroll + i, START_Y + i * ITEM_H); + _item_h = display.getLineHeight() + 1; + const int startY = 12; + _vis = (display.height() - startY) / _item_h; + + for (int i = 0; i < _vis && (_scroll + i) < SettingItem::Count; i++) { + renderItem(display, _scroll + i, startY + i * _item_h); } // scroll indicators if (_scroll > 0) { display.setColor(DisplayDriver::LIGHT); - display.setCursor(display.width() - 6, START_Y); + display.setCursor(display.width() - 6, startY); display.print("^"); } - if (_scroll + VISIBLE_ITEMS < SettingItem::Count) { + if (_scroll + _vis < SettingItem::Count) { display.setColor(DisplayDriver::LIGHT); - display.setCursor(display.width() - 6, START_Y + (VISIBLE_ITEMS - 1) * ITEM_H); + display.setCursor(display.width() - 6, startY + (_vis - 1) * _item_h); display.print("v"); } @@ -401,7 +405,7 @@ public: int next = nextSelectable(_selected); if (next != _selected) { _selected = next; - if (_selected >= _scroll + VISIBLE_ITEMS) _scroll = _selected - VISIBLE_ITEMS + 1; + if (_selected >= _scroll + _vis) _scroll = _selected - _vis + 1; } return true; } diff --git a/examples/companion_radio/ui-new/ToolsScreen.h b/examples/companion_radio/ui-new/ToolsScreen.h index 7ba49599..21d5226a 100644 --- a/examples/companion_radio/ui-new/ToolsScreen.h +++ b/examples/companion_radio/ui-new/ToolsScreen.h @@ -18,19 +18,23 @@ public: display.drawTextCentered(display.width() / 2, 0, "TOOLS"); display.fillRect(0, 10, display.width(), 1); - for (int i = 0; i < ITEM_COUNT; i++) { - int y = 12 + i * 12; + const int lineH = display.getLineHeight(); + const int itemH = lineH + 1; + const int startY = 12; + const int visible = (display.height() - startY) / itemH; + for (int i = 0; i < ITEM_COUNT && i < visible; i++) { + int y = startY + i * itemH; bool sel = (i == _sel); if (sel) { display.setColor(DisplayDriver::LIGHT); - display.fillRect(0, y - 1, display.width(), 11); + display.fillRect(0, y - 1, display.width(), itemH); display.setColor(DisplayDriver::DARK); } else { display.setColor(DisplayDriver::LIGHT); } display.setCursor(0, y); display.print(sel ? ">" : " "); - display.setCursor(8, y); + display.setCursor(display.getCharWidth() + 3, y); display.print(ITEMS[i]); } display.setColor(DisplayDriver::LIGHT);