diff --git a/DEVICES.md b/DEVICES.md index 117cb15..08332cd 100644 --- a/DEVICES.md +++ b/DEVICES.md @@ -48,6 +48,8 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases CardKB at address `0x5F` on the board I2C bus (GPIO4/3 on V4, GPIO17/18 on V4-R8). It provides text and focus navigation; the on-screen keyboard remains available when CardKB is absent or when the active field is tapped again. + V4-R8 also offers font-only Normal, Large and Huge text presets under Display; + controls retain their normal geometry so the compact screen stays navigable. - **Tanmatsu**: keyboard-driven UI (no touchscreen) with the coloured function keys mapped to tabs, ALT accent picker, UI scaling (Normal/Large/Huge), microSD for all persistent data. Ships through the Tanmatsu launcher store, diff --git a/include/lv_conf.h b/include/lv_conf.h index 34261b4..684abe7 100644 --- a/include/lv_conf.h +++ b/include/lv_conf.h @@ -56,11 +56,10 @@ #define LV_FONT_MONTSERRAT_12 1 #define LV_FONT_MONTSERRAT_14 1 #define LV_FONT_MONTSERRAT_16 1 -/* Larger Montserrat sizes for the large-screen boards' crisp "UI size" (Large/Huge) — render the - * UI bigger at native resolution instead of upscaling a low-res frame. Gated to the CAP_LARGE_SCREEN - * P4 boards (Tanmatsu + T-Display P4) so the flash-tighter S3 touch builds don't pay for fonts they - * never use. (device_caps.h isn't included here, so match on the board macros directly.) */ -#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) +/* Larger Montserrat sizes for crisp UI/text-size presets at native resolution. The P4 boards scale + * text and geometry; V4-R8 uses the same fonts without geometry scaling. Other flash-tighter S3 + * builds don't pay for fonts they never use. (Match board macros because device_caps.h isn't here.) */ +#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) || defined(HELTEC_LORA_V4_R8) #define LV_FONT_MONTSERRAT_18 1 #define LV_FONT_MONTSERRAT_20 1 #define LV_FONT_MONTSERRAT_24 1 diff --git a/src/helpers/esp32/TouchPrefsSchema.h b/src/helpers/esp32/TouchPrefsSchema.h index f7769b0..14d0263 100644 --- a/src/helpers/esp32/TouchPrefsSchema.h +++ b/src/helpers/esp32/TouchPrefsSchema.h @@ -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 diff --git a/src/helpers/esp32/TouchPrefsStore.cpp b/src/helpers/esp32/TouchPrefsStore.cpp index 8e0f212..b093330 100644 --- a/src/helpers/esp32/TouchPrefsStore.cpp +++ b/src/helpers/esp32/TouchPrefsStore.cpp @@ -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; diff --git a/src/helpers/esp32/TouchPrefsStore.h b/src/helpers/esp32/TouchPrefsStore.h index ff7a6bd..eb8c638 100644 --- a/src/helpers/esp32/TouchPrefsStore.h +++ b/src/helpers/esp32/TouchPrefsStore.h @@ -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); diff --git a/src/ui-touch/AppPage.h b/src/ui-touch/AppPage.h index 96716a0..471f4b2 100644 --- a/src/ui-touch/AppPage.h +++ b/src/ui-touch/AppPage.h @@ -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. */ diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 4f9aee7..5b336d0 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -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 diff --git a/src/ui-touch/device_caps.h b/src/ui-touch/device_caps.h index d86f7f3..3336018 100644 --- a/src/ui-touch/device_caps.h +++ b/src/ui-touch/device_caps.h @@ -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