Merge pull request #263 from oumike: Pager shortcuts, portrait layout fixes, theme accents

Minor general theme tweak, keyboard shortcuts for the pager build, UI tweaks on the heltec build.
This commit is contained in:
Kaj authored and GitHub committed 2026-08-13 09:11:19 +02:00
commit b733dbd99e
2 files changed
+140 -60

No files matched your search

+26 -6
View File
@@ -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 |
+114 -54
View File
@@ -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);
@@ -23085,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
@@ -23099,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;
}
@@ -23110,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).
@@ -23120,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)
@@ -23217,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);
@@ -23229,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);
@@ -28695,7 +28700,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
@@ -29013,8 +29018,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
@@ -34571,9 +34579,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() {
@@ -35051,13 +35058,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()) {
@@ -35178,14 +35182,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)
@@ -36637,6 +36655,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
@@ -36644,7 +36672,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)) {
@@ -36652,15 +36680,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;
}
@@ -40479,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);
@@ -40625,10 +40660,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)
@@ -40641,7 +40678,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).
@@ -41514,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
@@ -41561,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
@@ -43388,14 +43431,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
@@ -43413,7 +43462,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);
@@ -47741,6 +47794,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.