From eecc8219c4dd3e569cf994e356b4a2690aebfae9 Mon Sep 17 00:00:00 2001 From: "Michael A. Cojocari" Date: Wed, 12 Aug 2026 15:55:08 -0400 Subject: [PATCH 1/3] Wifi icon matches theme. --- src/ui-touch/UITask.cpp | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 815060c..17298d6 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -28990,8 +28990,11 @@ static void applyMapChrome(bool on) { applyBattColor(); // ...with the power-save amber overlaid if enabled if (g_statusbar.batt_pct) lv_obj_set_style_text_color(g_statusbar.batt_pct, fg_sub, LV_PART_MAIN); if (g_statusbar.clock) lv_obj_set_style_text_color(g_statusbar.clock, fg_sub, LV_PART_MAIN); - if (g_statusbar.conn_icon) lv_obj_set_style_text_color(g_statusbar.conn_icon, fg_sub, LV_PART_MAIN); - if (g_statusbar.ble_icon) lv_obj_set_style_text_color(g_statusbar.ble_icon, fg_sub, LV_PART_MAIN); + // The radio glyphs keep their accent tint off-map (matching the signal bars); on-map they + // join the rest of the chrome in black/off-white, which the light OSM tiles need for legibility. + const lv_color_t fg_radio = on ? fg_sub : lv_color_hex(COLOR_ACCENT); + if (g_statusbar.conn_icon) lv_obj_set_style_text_color(g_statusbar.conn_icon, fg_radio, LV_PART_MAIN); + if (g_statusbar.ble_icon) lv_obj_set_style_text_color(g_statusbar.ble_icon, fg_radio, LV_PART_MAIN); #if defined(HAS_TDECK_GT911) if (g_statusbar.sleep_icon) lv_obj_set_style_text_color(g_statusbar.sleep_icon, fg_sub, LV_PART_MAIN); #endif @@ -40553,10 +40556,12 @@ static void buildGlobalStatusBar() { #endif 0); - // Wi-Fi glyph (right of the Bluetooth glyph, left of the signal bars). + // Wi-Fi glyph (right of the Bluetooth glyph, left of the signal bars). Painted in + // the accent, matching the lit signal bars beside it — the glyph is only ever shown + // while the STA is connected, so accent always reads as "this radio is up". g_statusbar.conn_icon = lv_label_create(g_statusbar.root); lv_label_set_text(g_statusbar.conn_icon, ""); - lv_obj_set_style_text_color(g_statusbar.conn_icon, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_color(g_statusbar.conn_icon, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN); lv_obj_set_style_text_font(g_statusbar.conn_icon, &g_font_12, LV_PART_MAIN); lv_obj_align(g_statusbar.conn_icon, LV_ALIGN_RIGHT_MID, #if defined(TLORA_PAGER) @@ -40569,7 +40574,7 @@ static void buildGlobalStatusBar() { // Bluetooth glyph (left of the SD LED). Unified offset across all boards (see clock above). g_statusbar.ble_icon = lv_label_create(g_statusbar.root); lv_label_set_text(g_statusbar.ble_icon, ""); - lv_obj_set_style_text_color(g_statusbar.ble_icon, lv_color_hex(COLOR_SUB), LV_PART_MAIN); + lv_obj_set_style_text_color(g_statusbar.ble_icon, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN); lv_obj_set_style_text_font(g_statusbar.ble_icon, &g_font_12, LV_PART_MAIN); // Narrow bars (V4 portrait) have no DND slot beside BLE and their clock sits at -126, so BLE // stays at -111 there; wide bars keep -SC(127) (tight to the DND moon at -144). @@ -47669,6 +47674,13 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no // (Audio: the I2S speaker amp is installed on demand per tone — see // tdeckPlayNotify — so nothing to set up at boot.) + // Load the saved theme accent BEFORE the bar is built: the status bar is the one + // widget tree created here in begin() rather than in buildUiTree (which loads the + // accent itself), so without this its accent-tinted glyphs — the Wi-Fi/Bluetooth + // icons — would freeze the compile-time default instead of the user's colour. + // Idempotent: buildUiTree's own call just re-sets the same globals. + applyAccent(touchPrefsGetAccentColor()); + // Build the always-on top status bar AFTER the display driver is // registered — lv_layer_sys() needs an active disp or it returns // nullptr, and creating a child of nullptr was the boot-loop cause. From 7152400e4c47403277a642f54521bec297e8f70e Mon Sep 17 00:00:00 2001 From: "Michael A. Cojocari" Date: Wed, 12 Aug 2026 18:52:52 -0400 Subject: [PATCH 2/3] Keyboard shortcuts for the pager build. --- TLORA_PAGER_SHORTCUTS.md | 32 +++++++++++++--- src/ui-touch/UITask.cpp | 80 +++++++++++++++++++++++++++------------- 2 files changed, 81 insertions(+), 31 deletions(-) diff --git a/TLORA_PAGER_SHORTCUTS.md b/TLORA_PAGER_SHORTCUTS.md index 5dc433b..4f7a48a 100644 --- a/TLORA_PAGER_SHORTCUTS.md +++ b/TLORA_PAGER_SHORTCUTS.md @@ -21,6 +21,21 @@ Everything below only fires while you're **not** actively typing into a text field — if a field is focused and you're typing into it, letters type normally and these combos step out of the way. +## Bottom menu shortcuts + +The five main screens show their keyboard shortcuts directly in the bottom +bar: + +```text +[mail] M [contacts] C [home] H [map] A [settings] S +``` + +Press **M**, **C**, **H**, **A**, or **S** to jump to that screen. These +shortcuts only work while one of the five main screens is at its top level. +They do nothing inside an open chat, settings detail, app page, or popup, so a +letter cannot unexpectedly close the inner screen. They also remain inactive +while editing a text field, where the keys type normally. + ## Rotary encoder | Gesture | Action | Keyboard equivalent | @@ -28,16 +43,16 @@ normally and these combos step out of the way. | Turn | Move focus to the next/previous item on screen | **Fn (Alt) tapped alone** moves forward one step (NEXT only — no keyboard way to go backward) | | Short click | Select / confirm the focused item | **Enter** | | Hold ~1 s, then release | **Back**: closes a popup → closes an open chat → goes Home → Esc (whichever applies first) | **Backspace held ~1 s** | -| **Fn (Alt) + turn**, on a main tab | Jump directly between the 5 main tabs (Chats / Contacts / Home / Map / Settings) | none — encoder only | +| **Fn (Alt) + turn**, on a main tab | Move between the 5 main tabs (Mail / Contacts / Home / Map / Settings) | **M / C / H / A / S** jumps directly | | **Fn (Alt) + turn**, inside a settings page or chat | Scroll the page up/down | none — encoder only | | Turn, with a dropdown open | Scroll through the dropdown's options | none — encoder only | | Turn, with the accent picker open | Cycle through the accent variants (see below) | none — encoder only (Fn+Space to enter is shared) | | Turn, with the @-mention list open | Cycle through matching contacts (see below) | none — encoder only | | Turn, at the oldest/newest **loaded** message in an open chat | Loads more history in that direction and keeps going — see [Chat screen](#chat-screen) | none — encoder only | -The knob doubles as your only way to reach the bottom tab bar — since -there's no touch to tap an icon, **Fn+turn** while on any main tab is the -fastest way to switch sections. +Since there's no touch to tap the bottom bar, use its visible **M / C / H / A / +S** shortcuts to jump directly or **Fn+turn** to move through the tabs in +order. ## Chat screen @@ -193,9 +208,13 @@ While the **Map** tab is active: - **W** — pan north - **A** — pan west -- **S** — pan south +- **X** — pan south - **D** — pan east +`X` replaces the usual `S` position because **S** is the visible Settings +shortcut. Pressing **A** on another main screen opens Map; once Map is already +active, **A** pans west. + Each press shifts the view a quarter-screen and reloads tiles/markers as needed. @@ -216,6 +235,7 @@ dimmable brightness curve. | Input | Action | |---|---| +| M / C / H / A / S (top-level main screens only) | Open Mail / Contacts / Home / Map / Settings | | Turn encoder (or Fn tap = NEXT only) | Move focus | | Turn, at the loaded edge of a chat | Load more history, keep moving — only exits the list at the true oldest/newest message | | Click encoder (or Enter) | Select / confirm | @@ -236,4 +256,4 @@ dimmable brightness curve. | Space (double-tap) | Switch keyboard language | | Space (hold 1s) | Lock screen | | Q / E (slider focused) | Decrease / increase | -| W / A / S / D (Map tab) | Pan map north/west/south/east | +| W / A / X / D (Map tab) | Pan map north/west/south/east | diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index ab8d9bd..5d02d17 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -3719,10 +3719,9 @@ static void navRefocusFirstVisible(lv_obj_t* p) { static void navMenubarKeysSync() { #if defined(HAS_TANMATSU) || defined(TLORA_PAGER) // Tanmatsu menubar uses the coloured F-key shapes, not letter hotkeys. The - // pager has no such optional nav-mode toggle to hint at either (s_kbd_nav is - // T-Deck's trackball-nav-vs-touch concept, CAP_TRACKBALL-only, so it isn't - // even declared here) — no hints. A plain `return` isn't enough since the - // body below still needs s_kbd_nav to exist at compile time; exclude it. + // pager prints each fixed mnemonic beside its icon directly in the tab label, + // so neither target needs this optional overlay. A plain `return` isn't enough + // since the body below still needs s_kbd_nav to exist at compile time; exclude it. #else if (!g_lv.tabview) return; lv_obj_t* bar = lv_tabview_get_tab_btns(g_lv.tabview); @@ -28695,7 +28694,7 @@ static void mapCanvasEventCb(lv_event_t* e) { } #if defined(HAS_TANMATSU) || defined(TLORA_PAGER) -// Keyboard pan (Ctrl+Arrow on Tanmatsu / WASD on the pager, both on the Map +// Keyboard pan (Ctrl+Arrow on Tanmatsu / WAXD on the pager, both on the Map // tab). Mirrors the drag-release math in mapCanvasEventCb: synthesize a pixel // delta of ~1/4 the visible span in the arrow direction, convert it through // the same world-px ↔ lat/lon helpers (so the lon step automatically scales @@ -34574,9 +34573,8 @@ static void updateTrackball(unsigned long now) { #if defined(HAS_PAGER_ENCODER) || defined(HAS_PAGER_KEYBOARD) // "Back", extending the T-Deck/Tanmatsu back-key ladder: a popup/sheet on top // closes first, then a full-screen AppPage (Store, Lua apps, tools), then an -// open chat/channel detail, then Home (the pager has no dedicated Home hotkey -// and the bottom tab bar isn't a nav-group target, so a bare main tab otherwise -// had no way back to Home), else plain ESC. Shared by the rotary encoder's +// open chat/channel detail, then Home (the H mnemonic is deliberately inactive +// inside those nested views), else plain ESC. Shared by the rotary encoder's // long-press (updatePagerEncoder) and the keyboard's Backspace-hold alternative // (updatePagerBackspaceHold) so both agree exactly. static void pagerNavGoBack() { @@ -35054,13 +35052,10 @@ static void updatePagerEncoder(unsigned long now) { // Alt (the bottom-left orange key, otherwise a hold-only modifier for the // keyboard's symbol layer — free to reuse here since it types nothing on // its own) + turn jumps directly between the 5 main tabs (Chats/Contacts/ - // Home/Map/Settings). The bottom tab bar is deliberately not a nav-group - // focus target (same as T-Deck/Tanmatsu), and unlike those boards the - // pager has no separate dedicated hotkeys to reach it, so plain turning - // could otherwise only ever move focus WITHIN the current screen — - // reported: the tab bar icons were unreachable from Home. Scoped to the - // main-tab level (navOnMainPage()) — reported bug: this used to fire even - // inside a settings sheet/chat, silently abandoning it to jump tabs. + // Home/Map/Settings). This remains a sequential alternative to the direct + // M/C/H/A/S mnemonics. Scoped to the main-tab level (navOnMainPage()) — + // reported bug: this used to fire even inside a settings sheet/chat, + // silently abandoning it to jump tabs. for (; delta > 0; delta--) navSwitchTab(+1); for (; delta < 0; delta++) navSwitchTab(-1); } else if (pagerKeyboardAltHeld()) { @@ -35181,14 +35176,28 @@ static bool isDismissKey(int key) { // drawerPopupOpen() / anyPopupOpen() are defined just above the // HAS_TDECK_KEYBOARD block so the (ungated) gesture handlers can call them too. -// Bottom-tab a key jumps to (no popup, no field focused), or -1. Space/H Home, -// M Chats, C Contacts, L Map, S Settings. +// Bottom-tab a key jumps to (no popup, no field focused), or -1. static int tabForKey(int key) { +#if defined(TLORA_PAGER) + // Pager mnemonic keys are only a bottom-bar shortcut at the top level. In + // an open chat, settings detail, app page, or popup they remain inert so a + // letter cannot unexpectedly abandon the inner screen. + if (!navOnMainPage()) return -1; + switch (key) { + case 'm': case 'M': return CHAT_INBOX_TAB_INDEX; + case 'c': case 'C': return CONTACTS_TAB_INDEX; + case 'h': case 'H': return HOME_TAB_INDEX; + case 'a': case 'A': return MAP_TAB_INDEX; + case 's': case 'S': return SETTINGS_TAB_INDEX; + default: return -1; + } +#else // Old fixed letter tab-jumps (h/m/c/l/s) removed — tab jumps are now the // programmable keyboard-nav hotkeys (navTabForHotkey, default E/R/T/U/I), active // only while keyboard navigation is on. (void)key; return -1; +#endif } #endif // HAS_TDECK_KEYBOARD || HAS_PAGER_KEYBOARD || HAS_M9_KEYBOARD (keyboard helpers; the lock screen below is top-level) @@ -36640,6 +36649,16 @@ if (g_lv.task && g_lv.task->isManualLock()) { return; } } + // Bottom-bar mnemonic shortcuts: active only on a bare main tab (the + // navOnMainPage gate lives in tabForKey). Do this before Map panning so S + // can leave Map for Settings. If the requested tab is already active, let + // the key continue: A on Map remains the west-pan control. + const int pager_tab = tabForKey(key); + if (pager_tab >= 0 && pager_tab != getActiveTab()) { + goToTab(pager_tab); + if (g_lv.task) g_lv.task->noteUserInput(); + return; + } // Slider nudge: this board has no touch/trackball to drag a slider's // knob, so a focused lv_slider (Control Center brightness, a Settings // slider, the Map zoom bar, …) is otherwise stuck at whatever value it @@ -36647,7 +36666,7 @@ if (g_lv.task && g_lv.task->isManualLock()) { // (proportional ~20-presses-end-to-end step, live update + persist) — // the same adjustment the T-Deck trackball's LEFT/RIGHT already does — // rather than a fixed step that's wrong for every slider's range. (Was - // D/F; moved to Q/E to free up WASD for map panning below.) + // D/F; moved to Q/E to free up the map-panning keys below.) if (key == 'q' || key == 'Q' || key == 'e' || key == 'E') { lv_obj_t* focused = s_nav_group ? lv_group_get_focused(s_nav_group) : nullptr; if (focused && lv_obj_check_type(focused, &lv_slider_class)) { @@ -36655,15 +36674,16 @@ if (g_lv.task && g_lv.task->isManualLock()) { return; } } - // Map pan: WASD moves the map around on the Map tab (no touch/trackball + // Map pan: W/A/X/D moves the map around on the Map tab (no touch/trackball // to drag it), reusing the same mapNudge() step/re-render Tanmatsu's - // Ctrl+Arrow already uses. W=north A=west S=south D=east. + // Ctrl+Arrow already uses. X replaces S because S is now the Settings + // mnemonic. W=north A=west X=south D=east. if ((key == 'w' || key == 'W' || key == 'a' || key == 'A' || - key == 's' || key == 'S' || key == 'd' || key == 'D') && + key == 'x' || key == 'X' || key == 'd' || key == 'D') && getActiveTab() == MAP_TAB_INDEX) { switch (key) { case 'w': case 'W': mapNudge(0); break; - case 's': case 'S': mapNudge(1); break; + case 'x': case 'X': mapNudge(1); break; case 'a': case 'A': mapNudge(2); break; case 'd': case 'D': mapNudge(3); break; } @@ -43393,14 +43413,20 @@ static void buildUiTree() { lv_obj_add_event_cb(tab_btns, navMenubarSizeCb, LV_EVENT_SIZE_CHANGED, nullptr); // keep the keyboard-nav key hints positioned #endif - // Tab labels: icons-only (house, envelope, list, GPS pin, gear) — saves - // space and lets all five tabs sit comfortably in the 240px wide bar - // (240/5 = 48 px per icon). + // Tab labels: icons-only on touch targets; the 480px-wide Pager prefixes each + // icon with its physical-keyboard mnemonic so the shortcuts are discoverable. // Add order == index order: Chats(0), Contacts(1), Home(2, middle), Map(3), Settings(4). +#if defined(TLORA_PAGER) + lv_obj_t* tab_chats = lv_tabview_add_tab(g_lv.tabview, LV_SYMBOL_ENVELOPE " M"); + lv_obj_t* tab_contacts = lv_tabview_add_tab(g_lv.tabview, TOUCH_SYM_PERSON " C"); + lv_obj_t* tab_home = lv_tabview_add_tab(g_lv.tabview, LV_SYMBOL_HOME " H"); + lv_obj_t* tab_map = lv_tabview_add_tab(g_lv.tabview, LV_SYMBOL_GPS " A"); +#else lv_obj_t* tab_chats = lv_tabview_add_tab(g_lv.tabview, LV_SYMBOL_ENVELOPE); lv_obj_t* tab_contacts = lv_tabview_add_tab(g_lv.tabview, TOUCH_SYM_PERSON); // person icon (FA user) lv_obj_t* tab_home = lv_tabview_add_tab(g_lv.tabview, LV_SYMBOL_HOME); lv_obj_t* tab_map = lv_tabview_add_tab(g_lv.tabview, LV_SYMBOL_GPS); +#endif #if defined(HAS_EXPANSION_KIT) // Sensors tab is conditional: only add it (and shift Settings to index 5) when // an env sensor is present and the user pref is on. Otherwise Settings lands @@ -43418,7 +43444,11 @@ static void buildUiTree() { TAB_LAST = 4; } #endif +#if defined(TLORA_PAGER) + lv_obj_t* tab_settings = lv_tabview_add_tab(g_lv.tabview, LV_SYMBOL_SETTINGS " S"); +#else lv_obj_t* tab_settings = lv_tabview_add_tab(g_lv.tabview, LV_SYMBOL_SETTINGS); +#endif // Slightly larger font for icons so they're easy to tap. #if CAP_UI_SIZE lv_obj_set_style_text_font(tab_btns, &g_font_tab, LV_PART_MAIN); From 9a358954d1b8b52e7cc3f731b54800d3cdb58f69 Mon Sep 17 00:00:00 2001 From: "Michael A. Cojocari" Date: Wed, 12 Aug 2026 20:44:48 -0400 Subject: [PATCH 3/3] Heltec tweaks. --- src/ui-touch/UITask.cpp | 66 ++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 5d02d17..9e7c7df 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -23084,11 +23084,11 @@ static void homeControlPanelCb(lv_event_t* e) { // "Control panel" launcher -> // colour to use. static uint32_t homeStoreChipText(char* out, size_t cap) { if (s_msgs_write_fails) { // saves are failing right now - snprintf(out, cap, LV_SYMBOL_SAVE " FAIL x%u", (unsigned)s_msgs_write_fails); + snprintf(out, cap, "Save FAIL x%u", (unsigned)s_msgs_write_fails); return 0xE05252; // red — history is NOT landing } if (!s_seg_store_ready) { // old-format file not converted yet - snprintf(out, cap, LV_SYMBOL_SAVE " migrating"); + snprintf(out, cap, "Save migrating"); return 0xF5A623; // amber } // Healthy: WHEN the store last saved. A clock time answers "is my history @@ -23098,7 +23098,7 @@ static uint32_t homeStoreChipText(char* out, size_t cap) { // stopped saving yesterday can't read as fresh. char when[16]; chatSaveStamp(when, sizeof when, s_msgs_write_ok_epoch); - snprintf(out, cap, LV_SYMBOL_SAVE " %s", when); + snprintf(out, cap, "Saved %s", when); return COLOR_SUB; } @@ -23109,9 +23109,20 @@ static void makeHome(lv_obj_t* tab) { // Contacts / Settings. styleSurface(tab, COLOR_BG); lv_obj_set_style_pad_all(tab, 10, LV_PART_MAIN); - lv_obj_set_scroll_dir(tab, LV_DIR_NONE); - lv_obj_set_scrollbar_mode(tab, LV_SCROLLBAR_MODE_OFF); - lv_obj_clear_flag(tab, LV_OBJ_FLAG_SCROLLABLE); + + const bool home_land = chatLandscape(); + if (home_land) { + lv_obj_set_scroll_dir(tab, LV_DIR_NONE); + lv_obj_set_scrollbar_mode(tab, LV_SCROLLBAR_MODE_OFF); + lv_obj_clear_flag(tab, LV_OBJ_FLAG_SCROLLABLE); + } else { + // The live Expansion-Kit summary can add enough lines to push the charts + // and launcher row below the short portrait viewport. Let the page scroll + // instead of clipping those controls at the tab bar. + lv_obj_set_scroll_dir(tab, LV_DIR_VER); + lv_obj_set_scrollbar_mode(tab, LV_SCROLLBAR_MODE_AUTO); + lv_obj_add_flag(tab, LV_OBJ_FLAG_SCROLLABLE); + } // Content width = screen width minus the 10-px tab padding on each side. // Tracks rotation (220 portrait / 300 landscape). @@ -23119,7 +23130,6 @@ static void makeHome(lv_obj_t* tab) { // Landscape is short and wide: park the Send-advert button in the empty // right-hand strip (top-right) instead of a full-width bottom row, so the // TX/RX chart can use the freed vertical space and be ~2x taller. - const bool home_land = chatLandscape(); // Right-hand button column. Scales with the UI size so bigger labels ("Terminal") don't clip; // RSTRIP is the strip the left-hand content (status text + chart + info) must stay clear of. #if defined(TLORA_PAGER) @@ -23216,10 +23226,9 @@ static void makeHome(lv_obj_t* tab) { g_lv.home_env_chart = s_home_env_chart; lv_obj_set_size(s_home_env_chart, home_land ? (cw - RSTRIP) : cw, SC(34)); lv_obj_align(s_home_env_chart, LV_ALIGN_TOP_LEFT, 0, SC(92)); - lv_obj_clear_flag(s_home_env_chart, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_add_flag(s_home_env_chart, LV_OBJ_FLAG_CLICKABLE); - lv_obj_set_ext_click_area(s_home_env_chart, 6); - lv_obj_add_event_cb(s_home_env_chart, openLocalSensorsPageCb, LV_EVENT_CLICKED, nullptr); + // Keep the graph passive so a portrait drag beginning here scrolls Home. + // The summary text directly above remains the explicit Sensors link. + lv_obj_clear_flag(s_home_env_chart, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE); lv_chart_set_type(s_home_env_chart, LV_CHART_TYPE_LINE); lv_chart_set_point_count(s_home_env_chart, kHomeEnvHistoryPoints); lv_chart_set_update_mode(s_home_env_chart, LV_CHART_UPDATE_MODE_SHIFT); @@ -23228,10 +23237,7 @@ static void makeHome(lv_obj_t* tab) { lv_chart_set_div_line_count(s_home_env_chart, 0, 0); lv_obj_set_style_bg_color(s_home_env_chart, lv_color_hex(COLOR_BG), LV_PART_MAIN); lv_obj_set_style_bg_opa(s_home_env_chart, LV_OPA_COVER, LV_PART_MAIN); - lv_obj_set_style_border_color(s_home_env_chart, lv_color_hex(COLOR_ACCENT), LV_PART_MAIN); - lv_obj_set_style_border_opa(s_home_env_chart, LV_OPA_20, LV_PART_MAIN); - lv_obj_set_style_border_width(s_home_env_chart, 1, LV_PART_MAIN); - lv_obj_set_style_radius(s_home_env_chart, 6, LV_PART_MAIN); + lv_obj_set_style_border_width(s_home_env_chart, 0, LV_PART_MAIN); lv_obj_set_style_size(s_home_env_chart, 0, LV_PART_INDICATOR); s_home_env_batt = lv_chart_add_series(s_home_env_chart, lv_color_hex(0x4F9DF7), LV_CHART_AXIS_PRIMARY_Y); s_home_env_temp = lv_chart_add_series(s_home_env_chart, lv_color_hex(0xF5A623), LV_CHART_AXIS_SECONDARY_Y); @@ -40502,8 +40508,14 @@ static void buildGlobalStatusBar() { const lv_coord_t BH = 20, BW = 26, GAP = 3, BX0 = 6; const lv_coord_t BY = (STATUSBAR_H - BH) / 2; #else - const lv_coord_t BH = (lv_coord_t)((STATUSBAR_H * 2 - 4) * 7 / 10); - const lv_coord_t BW = 34, GAP = 4, BX0 = 6, BY = (lv_coord_t)(STATUSBAR_H * 2 - BH) / 2; + const bool portrait = lv_disp_get_hor_res(nullptr) < lv_disp_get_ver_res(nullptr); + const lv_coord_t BH = portrait ? 20 : (lv_coord_t)((STATUSBAR_H * 2 - 4) * 7 / 10); + const lv_coord_t BW = portrait ? 30 : 34; + const lv_coord_t GAP = 4, BX0 = 6; + // Portrait's clock occupies row 1. Keep the Mail actions wholly in row 2; + // centering them across both rows put their upper half through the time. + const lv_coord_t BY = portrait ? STATUSBAR_H + (STATUSBAR_H - BH) / 2 + : (lv_coord_t)(STATUSBAR_H * 2 - BH) / 2; #endif auto mk = [&](int slot_from_left) -> lv_obj_t* { lv_obj_t* b = lv_btn_create(g_statusbar.root); @@ -41539,14 +41551,13 @@ static void relayoutHomeCharts() { if (!g_lv.home_env || !s_home_env_chart || !s_home_chart_legend) return; const bool home_land = lv_disp_get_hor_res(nullptr) > lv_disp_get_ver_res(nullptr); - const int cw = tabContentW(); + // makeHome() gives the tab 10 px of padding on each side. Stay inside that + // content box; using the full screen width made the chart's top/right frame + // look like a stray L drawn across the portrait screen. + const int cw = tabContentW() - 20; const int BTNW = SC(100); const int RSTRIP = BTNW + 10; -#if defined(HAS_TDECK_GT911) || defined(HAS_TANMATSU) || defined(HAS_RAK_TAP_V2) || defined(ATTAKY_MESH_SERIES) const int chart_w = home_land ? (cw - RSTRIP) : cw; -#else - const int chart_w = cw; -#endif // The env summary line is hard-placed at SC(58) in the tree, which collides with // the (up to two-line, WRAP) stats line on the narrow V4 portrait — the stats line @@ -41586,8 +41597,15 @@ static void relayoutHomeCharts() { } if (s_home_adv_btn && !home_land) { - lv_obj_set_pos(s_home_adv_btn, 0, legend_y + 16 + chart_h + 8); - lv_obj_set_width(s_home_adv_btn, cw); + const int button_y = legend_y + 16 + chart_h + 8; + const int button_gap = 8; + const int button_w = (cw - button_gap) / 2; + lv_obj_set_pos(s_home_adv_btn, 0, button_y); + lv_obj_set_size(s_home_adv_btn, button_w, 36); + if (g_lv.home_apps && lv_obj_is_valid(g_lv.home_apps)) { + lv_obj_set_pos(g_lv.home_apps, button_w + button_gap, button_y); + lv_obj_set_size(g_lv.home_apps, button_w, 36); + } } } #endif // HAS_EXPANSION_KIT