fix(v4-r8): improve touch and text accessibility

This commit is contained in:
Michael A. Cojocari
2026-09-18 16:44:12 -04:00
parent bf114b4618
commit eb4b71fb67
8 changed files with 108 additions and 30 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
namespace TouchPrefsSchema {
static constexpr uint16_t MAGIC = 0x5743; // 'WC' (WadaCfg)
static constexpr uint8_t CURRENT_VERSION = 62; // v49: fem_lna default flip on the V4-R8; v50: map tile z/x/y line off by default (no new fields); v51: console_mode (boot into the text console; new trailing field, default OFF); v52: console_monitor (show incoming messages in the console, default ON); v54: boot_wifi_time + boot_wifi_open (cold-boot saved-Wi-Fi time sync, #383; both OFF); v55: loud_alerts (resonant-pitch chime, #388; OFF); v56: theme_mode (Day/Night firmware theme, #296; default Night); v57: gps_fuzz_m (displace the advertised position, #399; 0 = off); v58: Attaky notification blink enable + room/DM color indexes (#423); v59: telem_loc_exact (answer position requests from chosen contacts with the REAL fix instead of the advert displacement; 0 = keep it displaced); v60: home_key_keeps_drawer (#491; default OFF); v61: ble_kbd_* (Bluetooth serves a keyboard instead of the phone app, its layout and the paired keyboard; default phone app); v62: ble_kbd_back (a second key that acts as Back; 0 = only Esc)
static constexpr uint8_t CURRENT_VERSION = 63; // v49: fem_lna default flip on the V4-R8; v50: map tile z/x/y line off by default (no new fields); v51: console_mode (boot into the text console; new trailing field, default OFF); v52: console_monitor (show incoming messages in the console, default ON); v54: boot_wifi_time + boot_wifi_open (cold-boot saved-Wi-Fi time sync, #383; both OFF); v55: loud_alerts (resonant-pitch chime, #388; OFF); v56: theme_mode (Day/Night firmware theme, #296; default Night); v57: gps_fuzz_m (displace the advertised position, #399; 0 = off); v58: Attaky notification blink enable + room/DM color indexes (#423); v59: telem_loc_exact (answer position requests from chosen contacts with the REAL fix instead of the advert displacement; 0 = keep it displaced); v60: home_key_keeps_drawer (#491; default OFF); v61: ble_kbd_* (Bluetooth serves a keyboard instead of the phone app, its layout and the paired keyboard; default phone app); v62: ble_kbd_back (a second key that acts as Back; 0 = only Esc); v63: V4-R8 ui_scale reset before exposing font-only presets (#536)
static constexpr uint8_t BROKEN_MID_INSERT_VERSION = 44;
// Persisted byte layout. New fields must be appended at the end: older blobs
+7 -2
View File
@@ -112,8 +112,8 @@ static void cfgSetDefaults(TouchCfg& c) {
c.map_show_tilexyz = 0; // v50: the "z12 12/2105/1376" tile-path line is developer clutter on the map; opt-in via Map options
c.map_show_contacts = 1;
c.app_grid_large = 0; // default: compact app grid (T-Deck 4 cols / V4 3 cols)
#if defined(TLORA_PAGER)
c.ui_scale = 0; // Pager: Small/current typography by default
#if defined(TLORA_PAGER) || defined(HELTEC_LORA_V4_R8)
c.ui_scale = 0; // Font-only presets start at the existing typography.
#else
c.ui_scale = 1; // large-screen boards keep their existing 150% default
#endif
@@ -280,6 +280,11 @@ static void cfgLoadOrMigrate() {
memset(s_cfg.ble_kbd_name, 0, sizeof s_cfg.ble_kbd_name);
}
if (stored_version < 62) s_cfg.ble_kbd_back = 0; // only Esc goes back
#if defined(HELTEC_LORA_V4_R8)
// ui_scale existed before the R8 exposed a selector and inherited the
// unrelated large-screen default. Preserve its current appearance once.
if (stored_version < 63) s_cfg.ui_scale = 0;
#endif
if (stored_version < 58) {
s_cfg.attaky_notify_enabled = 0;
s_cfg.attaky_notify_room_color = 0;
+2 -1
View File
@@ -163,7 +163,8 @@ bool touchPrefsGetSleepIdle();
bool touchPrefsSetSleepIdle(bool on);
/* UI-size preset. Large-screen boards retain their 0..2 percentage mapping;
* the T-Pager exposes four 0..3 semantic font presets. Reboot to apply. */
* V4-R8 uses 0..2 font-only presets, and T-Pager uses four 0..3 semantic font
* presets. Reboot to apply. */
uint8_t touchPrefsGetUiScale();
bool touchPrefsSetUiScale(uint8_t scale);
+1 -1
View File
@@ -30,7 +30,7 @@
// own title and close button UNDERNEATH the real status bar with no reachable exit. Going
// through here instead means a page can never disagree with the bar about its own height.
/** y of the first usable content row: immediately under the (possibly tall) status bar. */
/** y of the first usable content row below the target's reserved status-bar area. */
lv_coord_t appPageContentTop();
/** Usable content height below the status bar. */
+88 -17
View File
@@ -910,6 +910,28 @@ static void initTouchFontFallbacks() {
break;
}
g_font_tab = lv_font_montserrat_16;
#elif defined(HELTEC_LORA_V4_R8)
// Accessible semantic text without scaling 240x320 geometry. Rows, cards and
// controls retain their established dimensions and remain scrollable.
s_ui_fscale = 100;
switch (touchPrefsGetUiScale()) {
case 1:
g_font_12 = lv_font_montserrat_16;
g_font_14 = lv_font_montserrat_18;
g_font_16 = lv_font_montserrat_20;
break;
case 2:
g_font_12 = lv_font_montserrat_18;
g_font_14 = lv_font_montserrat_20;
g_font_16 = lv_font_montserrat_24;
break;
default:
g_font_12 = lv_font_montserrat_12;
g_font_14 = lv_font_montserrat_14;
g_font_16 = lv_font_montserrat_16;
break;
}
g_font_tab = lv_font_montserrat_16;
#elif CAP_LARGE_SCREEN
// Crisp "UI size": render bigger by swapping in larger built-in Montserrat fonts (NOT by
// upscaling a low-res frame). g_font_12/14/16 are what the whole UI draws with, so this scales
@@ -1708,9 +1730,17 @@ static inline lv_coord_t statusBarCurH() { return STATUSBAR_H; }
#else
static inline lv_coord_t statusBarCurH() { return s_statusbar_tall ? (lv_coord_t)(STATUSBAR_H * 2) : STATUSBAR_H; }
#endif
#if defined(HELTEC_LORA_V4_R8)
static void r8ResizeContentBelowStatusBar(lv_coord_t top);
#endif
static void statusBarSetTall(bool tall) {
s_statusbar_tall = tall;
if (g_statusbar.root) lv_obj_set_height(g_statusbar.root, statusBarCurH());
#if defined(HELTEC_LORA_V4_R8)
// R8 portrait mapping is identity, but a tall clickable bar used to cover the
// first row of the tab view. Keep visible content below its live hitbox.
r8ResizeContentBelowStatusBar(statusBarCurH());
#endif
// (updateGlobalStatusBar drives this every tick + refreshes the left zone; it must
// NOT be called from here — it calls back into statusBarSetTall = recursion.)
}
@@ -1721,8 +1751,18 @@ void reserveTileFetchStack(); // fwd: claim the worker stack before Wi-Fi eats
// Thin wrappers over the machinery just above, exported so the self-contained app
// modules (SnakeGame) build the same page as the in-file tool
// pages instead of hand-rolling it against a hardcoded bar height.
lv_coord_t appPageContentTop() { return STATUSBAR_H; }
lv_coord_t appPageContentH() { return (lv_coord_t)(lv_disp_get_ver_res(nullptr) - STATUSBAR_H); }
lv_coord_t appPageContentTop() {
#if defined(HELTEC_LORA_V4_R8)
// appPageCreateRoot() runs before appPageBegin(), so reserve the height the
// latter is about to activate rather than reading the current one-row state.
return (lv_coord_t)(STATUSBAR_H * 2);
#else
return STATUSBAR_H;
#endif
}
lv_coord_t appPageContentH() {
return (lv_coord_t)(lv_disp_get_ver_res(nullptr) - appPageContentTop());
}
lv_obj_t* appPageCreateRoot(uint32_t bg_color) {
lv_obj_t* root = lv_obj_create(lv_layer_top());
@@ -2000,8 +2040,8 @@ static inline lv_coord_t chatBarH() { return STATUSBAR_H; }
#else
static inline lv_coord_t chatBarH() { return (lv_coord_t)(STATUSBAR_H * 2); }
#endif
#if defined(HAS_TDECK_PRO)
// E-paper cannot render the glass title row cleanly, so chat content starts below it.
#if defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8)
// E-paper and R8 content start below the complete status-bar hitbox.
static inline lv_coord_t chatContentTop(){ return chatBarH(); }
#else
static inline lv_coord_t chatContentTop(){ return STATUSBAR_H; }
@@ -2634,6 +2674,19 @@ static uint16_t* s_scale_buf = nullptr; // upscale s
// ---- Global UI state instance ----
LvUiState g_lv = {};
#if defined(HELTEC_LORA_V4_R8)
static void r8ResizeContentBelowStatusBar(lv_coord_t top) {
if (!g_lv.tabview) return;
lv_obj_set_pos(g_lv.tabview, 0, top);
lv_obj_set_size(g_lv.tabview, lv_disp_get_hor_res(nullptr),
lv_disp_get_ver_res(nullptr) - top);
if (g_lv.dm.list_cont) {
lv_obj_set_size(g_lv.dm.list_cont, lv_disp_get_hor_res(nullptr),
lv_disp_get_ver_res(nullptr) - top - TABBAR_H);
}
}
#endif
/** If `cp` has no glyph in `font`, show '*' instead of LVGL's missing-glyph box. Preserves \\n \\r \\t. */
static bool uiFontHasGlyph(const lv_font_t* font, uint32_t cp) {
if (!font) return true;
@@ -14862,6 +14915,8 @@ static void buildDeviceSettings(int sec) {
lv_obj_t* dd = lv_dropdown_create(body);
#if defined(TLORA_PAGER)
lv_dropdown_set_options(dd, TR("Small\nMedium\nLarge\nJumbo"));
#elif defined(HELTEC_LORA_V4_R8)
lv_dropdown_set_options(dd, TR("Normal\nLarge text\nHuge text"));
#else
lv_dropdown_set_options(dd, TR("Normal (100%)\nLarge (150%)\nHuge (200%)"));
#endif
@@ -27496,7 +27551,11 @@ static void openFileTransferPage() {
const lv_coord_t sw = lv_disp_get_hor_res(nullptr);
s_file_transfer_root = appPageCreateRoot(COLOR_BG);
#if defined(HELTEC_LORA_V4_R8)
lv_obj_set_style_pad_top(s_file_transfer_root, 10, LV_PART_MAIN);
#else
lv_obj_set_style_pad_top(s_file_transfer_root, STATUSBAR_H + 10, LV_PART_MAIN);
#endif
lv_obj_set_style_pad_left(s_file_transfer_root, 14, LV_PART_MAIN);
lv_obj_set_style_pad_right(s_file_transfer_root, 14, LV_PART_MAIN);
lv_obj_set_style_pad_bottom(s_file_transfer_root, 14, LV_PART_MAIN);
@@ -35544,7 +35603,8 @@ static void makeChatDetail(LvChatPanel& p) {
p.overlay = lv_obj_create(lv_scr_act());
lv_obj_set_size(p.overlay, chatScreenW(), chatScreenH());
// Most boards start below the solid row and let the glass title row overlap the
// list. T-Deck Pro starts below both rows because e-paper needs opaque separation.
// list. T-Deck Pro and R8 start below both rows: e-paper needs opaque separation,
// while R8 must not show tappable content beneath the bar's touch hitbox.
lv_obj_set_pos(p.overlay, 0, chatContentTop());
styleSurface(p.overlay, COLOR_BG, 0);
lv_obj_set_style_pad_all(p.overlay, 0, LV_PART_MAIN); // prevent child-position offset
@@ -35577,7 +35637,7 @@ static void makeChatDetail(LvChatPanel& p) {
// The status bar's glass lower row floats over the TOP of the list on boards
// with a two-row chat header. Pager keeps the chat header in one regular row,
// so retaining that old row-sized inset only wastes message space.
#if defined(TLORA_PAGER) || defined(HAS_TDECK_PRO)
#if defined(TLORA_PAGER) || defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8)
lv_obj_set_style_pad_top(p.msgs, 6, LV_PART_MAIN);
#else
lv_obj_set_style_pad_top(p.msgs, STATUSBAR_H + 6, LV_PART_MAIN);
@@ -35623,7 +35683,7 @@ static void makeChatDetail(LvChatPanel& p) {
lv_obj_set_style_border_width(p.jump_oldest_btn, 0, LV_PART_MAIN);
lv_obj_set_style_outline_width(p.jump_oldest_btn, 0, LV_PART_MAIN);
lv_obj_set_ext_click_area(p.jump_oldest_btn, 6);
#if defined(HAS_TDECK_PRO)
#if defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8)
lv_obj_set_pos(p.jump_oldest_btn, chatScreenW() - 28, CHAT_HDR_H + 2);
#else
lv_obj_set_pos(p.jump_oldest_btn, chatScreenW() - 28, CHAT_HDR_H + STATUSBAR_H + 2);
@@ -36462,20 +36522,25 @@ static void openSettingsCategory(int cat) {
const lv_coord_t sw = lv_disp_get_hor_res(nullptr);
const lv_coord_t sh = lv_disp_get_ver_res(nullptr);
// On the BASE screen layer (like the chat overlay), so the status bar — which lives on
// lv_layer_top above it — paints its glass lower row OVER this sheet and the page
// content scrolls UNDER the bar. (On lv_layer_top the sheet would sit above the bar and
// the glass would reveal the settings landing behind the bar instead of this page.)
// On the BASE screen layer (like the chat overlay), so the status bar remains
// above it. The R8 keeps the entire sheet below the bar's live touch hitbox.
lv_obj_t* root = lv_obj_create(lv_scr_act());
lv_obj_remove_style_all(root);
// Settings detail pages use a DOUBLE-height status bar (its back chevron + title become
// a tall title bar). Set the category first so the bar paints the title + goes tall.
// The sheet starts under the SOLID top row (STATUSBAR_H); the page insets its top by
// the glass lower-row height so content rests below the bar but scrolls under it.
// Most boards start under the solid row and inset the page below the glass row.
// R8 starts the sheet itself below both rows so no visible content is untappable.
s_settings_open_cat = cat;
statusBarSetTall(true);
lv_obj_set_size(root, sw, sh - STATUSBAR_H);
lv_obj_set_pos(root, 0, STATUSBAR_H);
#if defined(HELTEC_LORA_V4_R8)
const lv_coord_t settings_top = statusBarCurH();
const lv_coord_t settings_pad_top = 8;
#else
const lv_coord_t settings_top = STATUSBAR_H;
const lv_coord_t settings_pad_top = STATUSBAR_H + 8;
#endif
lv_obj_set_size(root, sw, sh - settings_top);
lv_obj_set_pos(root, 0, settings_top);
styleSurface(root, COLOR_BG, 0);
lv_obj_set_style_pad_all(root, 0, LV_PART_MAIN);
lv_obj_clear_flag(root, LV_OBJ_FLAG_SCROLLABLE);
@@ -36484,9 +36549,9 @@ static void openSettingsCategory(int cat) {
lv_obj_t* page = lv_obj_create(root);
lv_obj_set_pos(page, 0, 0);
lv_obj_set_size(page, sw, sh - STATUSBAR_H);
lv_obj_set_size(page, sw, sh - settings_top);
prepSettingsPage(page);
lv_obj_set_style_pad_top(page, STATUSBAR_H + 8, LV_PART_MAIN); // clear the glass lower bar row
lv_obj_set_style_pad_top(page, settings_pad_top, LV_PART_MAIN);
resetSettingsModalState();
s_settings_page = page;
@@ -58126,6 +58191,12 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
STATUSBAR_H = SB_TOP_PAD + SB_ROW * 2; // top safe-area + two rows (round phone panel)
#else
STATUSBAR_H = SC(22); // grow the status bar to fit bigger text at Large/Huge (no-op at 100%)
#if defined(HELTEC_LORA_V4_R8)
// Geometry stays fixed for R8 text presets, but Huge's 20 px title role
// needs two extra pixels to avoid clipping regular one-row status text.
const lv_coord_t text_bar_h = lv_font_get_line_height(&g_font_14) + 4;
if (STATUSBAR_H < text_bar_h) STATUSBAR_H = text_bar_h;
#endif
#endif
// Allocate the draw buffer in PSRAM so the ~12 KB it costs comes out of
// the 8 MB external RAM instead of the 320 KB internal DRAM that WiFi
+3 -3
View File
@@ -181,9 +181,9 @@
#endif
// Persisted, restart-to-apply UI-size selector. Large-screen boards already
// expose it; the Pager adds font-only presets because its 480x222 viewport is
// wide enough for larger type but too short for global geometry scaling.
#if CAP_LARGE_SCREEN || defined(TLORA_PAGER)
// expose it; the Pager and V4-R8 add font-only presets because their compact
// viewports cannot safely take global geometry scaling.
#if CAP_LARGE_SCREEN || defined(TLORA_PAGER) || defined(HELTEC_LORA_V4_R8)
#define CAP_UI_SIZE 1
#else
#define CAP_UI_SIZE 0