Touch UI follow-ups 4: M9 composer, Wio time sync, P4 one-row header, keyboard/emoji fixes

- M9 chat: move # and emoji buttons left of the text input (QR | # | emoji | text | Send)
- Wio Tracker L2: enable Clock & time sync settings (CAP_BOOT_TIME_SYNC / BootTimeSync)
- P4: lift chat composer above the bottom screen curve
- P4: one-row status bar (no BLE icon, no battery %), title left, clock beside icons;
  bottom-aligned Wi-Fi/signal/battery glyphs; stacked chat header only while a chat is open
- Touch keyboard: non-letter keys use theme text colour and letter-key font in dark mode
- T-Deck Pro: remove "Roll to highlight" hint from emoji picker (fixes overlapping text)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Michael A. CojocariandCopilot committed 2026-10-07 11:09:18 -04:00
1 parent 39f342c67e
commit 4844a9bd90
6 files changed
+347 -191

No files matched your search

+3 -1
View File
@@ -98,7 +98,9 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases
mode manually and retry, and please report it.
- **Seeed Wio Tracker L2**: pre-release touch target under active bring-up. Builds are for
development and hardware validation only; no public release artifact is
promised until the port is verified.
promised until the port is verified. It has no battery-backed clock, so the
same optional saved-Wi-Fi cold-boot time sync offered on T-Deck is available
in Clock settings.
- **CrowPanel Advance 3.5**: `crowpanel_35_companion_radio_touch` is a
16 MB flash / 8 MB OPI PSRAM build for the ILI9488/GT911 panel and an SX1262
in the expansion slot. It starts upright in 480x320 landscape; Display
+3 -2
View File
@@ -97,8 +97,9 @@ appears at the right edge. Press **Right** to select it, then **OK** to jump to
the newest message.
Channels, direct messages, and room conversations place **#** and emoji to the
right of the message input. Press Right to move from the input to **#**, and
Right again to move to emoji. Press **OK** on either button to open its picker.
left of the message input. With the caret at the start of the input, press Left
to move to emoji, and Left again to move to **#**; Right walks back to the
input. Press **OK** on either button to open its picker.
In the symbol grid, use all four arrows to move the highlight and **OK** to
insert the selected character.
+3 -2
View File
@@ -18,8 +18,9 @@
// the M9 (RTC integrity can be lost across its hard power cut). Keep the runtime
// gate as narrow as the UI capability gate so a migrated preference file cannot
// unexpectedly start Wi-Fi on another board.
// The T-Display P4 joins on its esp-hosted build (see CAP_BOOT_TIME_SYNC).
#if defined(HAS_TDECK_GT911) || defined(HAS_THINKNODE_M9) || \
// The T-Display P4 joins on its esp-hosted build (see CAP_BOOT_TIME_SYNC), and the
// Wio Tracker L2 (no RTC) joins too.
#if defined(HAS_TDECK_GT911) || defined(HAS_THINKNODE_M9) || defined(HAS_WIO_TRACKER_L2) || \
(defined(HAS_TDISPLAY_P4) && TDP4_C6_HOSTED)
#define BOOT_TIME_SYNC_SUPPORTED 1
#else
+2 -2
View File
@@ -25,8 +25,8 @@
//
// These exist because the self-contained app modules (SnakeGame, the Lua host) hand-rolled
// the chrome against a hardcoded `kTopBar = 22`. STATUSBAR_H is a runtime value — SC(22)
// once the UI scale is above 100%, and SB_TOP_PAD + SB_ROW*2 on the T-Display P4's
// two-row bar — so on those boards the pages were positioned too high, which put their
// once the UI scale is above 100%, and SB_TOP_PAD + SB_ROW on the T-Display P4's
// round-cornered bar — so on those boards the pages were positioned too high, which put their
// 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.
+333 -182
View File
@@ -2011,18 +2011,18 @@ constexpr int SWIPE_SCROLL_STEP = 90;
// once at boot in begin(), before the UI is built. Stays 22 on the non-scaled boards.
static lv_coord_t STATUSBAR_H = 22;
#if CAP_ROUND_CORNERS
// Two-row status bar for the round-cornered phone panel: row 1 (top) holds the app name +
// clock, row 2 (bottom) holds the wifi/ble/sd/signal/battery cluster. A top pad clears the
// corner arc; a horizontal inset keeps the end glyphs out of the left/right arcs. STATUSBAR_H
// is set to (top pad + two rows) at boot so every page reserves the taller bar, and the bar
// stays this fixed height in ALL states (no *2 doubling — the tall personalities reuse the
// two rows). Text tops (ROW1_Y / ROW2_Y) are tuned so the small g_font glyphs sit centred.
// One-row status bar for the round-cornered phone panel: the profile name / page title on
// the left, then the clock, Wi-Fi, signal and battery packed on the right, all on one line.
// A top pad clears the corner arc; a horizontal inset keeps the end glyphs out of the
// left/right arcs. STATUSBAR_H is set to (top pad + one row) at boot. An open chat is the
// one exception: its header (title over subtitle) grows the bar to SB_CHAT_H.
static constexpr lv_coord_t SB_ROW = 22; // one status-bar row
static constexpr lv_coord_t SB_TOP_PAD = 8; // clear the top corner arc
static constexpr lv_coord_t SB_INSET_X = 16; // left/right corner safe-area
static constexpr lv_coord_t SB_ROW1_Y = SB_TOP_PAD + 2; // ~10 text top, row 1 (clock)
static constexpr lv_coord_t SB_ROW2_Y = SB_TOP_PAD + SB_ROW + 3; // ~33 text top, row 2 (name + status)
static void statusBarLayoutTwoRow(int slide); // defined just after buildGlobalStatusBar
static constexpr lv_coord_t SB_ROW1_Y = SB_TOP_PAD + 2; // ~10 text top of the row (clock)
static constexpr lv_coord_t SB_CHAT_H = SB_TOP_PAD + SB_ROW * 2; // open-chat header height
static bool s_round_chat_bar = false; // bar grown to SB_CHAT_H
static void statusBarLayoutOneRow(); // defined just after buildGlobalStatusBar
#endif
// True while the Reader/Web page is collapsed and showing its URL in the status bar's
// title zone — updateGlobalStatusBar then hides the clock to make room for the URL.
@@ -2093,9 +2093,9 @@ static void (*s_apppage_close)() = nullptr;
// right at the top of its content, which the glass row used to sit over).
static bool s_apppage_slim = false;
#if CAP_ROUND_CORNERS
// The round-panel bar is already two rows tall in every state — the tall personalities
// (settings title, inbox actions, open chat) reuse the two rows rather than doubling it.
static inline lv_coord_t statusBarCurH() { return STATUSBAR_H; }
// The round-panel bar is one row in every state except an open chat (see SB_CHAT_H);
// settings / tool pages keep their "‹ Title" on that one row rather than doubling it.
static inline lv_coord_t statusBarCurH() { return s_round_chat_bar ? SB_CHAT_H : STATUSBAR_H; }
#else
static inline lv_coord_t statusBarCurH() { return s_statusbar_tall ? (lv_coord_t)(STATUSBAR_H * 2) : STATUSBAR_H; }
#endif
@@ -2346,8 +2346,22 @@ static inline bool chatComposerTwoRow() {
static inline lv_coord_t chatComposerTopRowH() {
return chatComposerTwoRow() ? (lv_coord_t)(CHAT_COMP_BTN_P4 + 4) : 0;
}
// Portrait P4: the full-width text box would otherwise sit under the bottom corner
// arcs, so the whole composer rides this far above the screen's bottom edge (the
// row's bottom padding). Matches the corner safe-area the status bar uses.
static inline lv_coord_t chatComposerBottomLift() {
#if CAP_ROUND_CORNERS
return chatComposerTwoRow() ? SB_INSET_X : 0;
#else
return 0;
#endif
}
// Composer row height not available to the text box: row padding, the top row, the lift.
static inline lv_coord_t chatComposerChromeH() {
return 4 + chatComposerTopRowH() + chatComposerBottomLift();
}
static inline lv_coord_t chatComposerBaseH() {
if (chatComposerTwoRow()) return CHAT_COMP_H + chatComposerTopRowH();
if (chatComposerTwoRow()) return CHAT_COMP_H + chatComposerTopRowH() + chatComposerBottomLift();
#if defined(TLORA_PAGER)
// Preserve the 34-px Small composer, then add enough chrome around the live
// font line for Medium/Large without globally scaling the short viewport.
@@ -2499,16 +2513,18 @@ static inline lv_coord_t chatComposerTaW(bool channel_mode) {
return width;
}
// Most boards use a DOUBLE-height status bar in a chat (thread name on the lower
// row + a centred cog). The round panel already has two physical rows, while the
// short Pager keeps its back/cog/title in the regular single row. chatBarH() is
// the full visual height used by channel/blocked sheets.
#if CAP_ROUND_CORNERS || defined(TLORA_PAGER)
// row + a centred cog). The round panel grows its one-row bar to SB_CHAT_H for the
// chat header, while the short Pager keeps its back/cog/title in the regular single
// row. chatBarH() is the full visual height used by channel/blocked sheets.
#if CAP_ROUND_CORNERS
static inline lv_coord_t chatBarH() { return SB_CHAT_H; }
#elif defined(TLORA_PAGER)
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) || defined(HELTEC_LORA_V4_R8)
// E-paper and R8 content start below the complete status-bar hitbox.
#if defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8) || CAP_ROUND_CORNERS
// E-paper, R8 and the round panel start chat content below the complete chat header.
static inline lv_coord_t chatContentTop(){ return chatBarH(); }
#else
static inline lv_coord_t chatContentTop(){ return STATUSBAR_H; }
@@ -8789,7 +8805,7 @@ static void chatComposerAutoGrow(LvChatPanel* p) {
// V4 with the keyboard down) it sits at the screen bottom.
const bool kb = g_lv.keyboard && !lv_obj_has_flag(g_lv.keyboard, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_height(p->composer_row, s_comp_h);
lv_obj_set_height(p->composer_ta, s_comp_h - 4 - chatComposerTopRowH());
lv_obj_set_height(p->composer_ta, s_comp_h - chatComposerChromeH());
lv_obj_set_y(p->composer_row, kb ? chatCompYKb() : chatCompYOpen());
lv_obj_set_height(p->msgs, kb ? chatMsgHKb() : chatMsgHOpen());
// Grow the bottom inset with the composer so the newest bubble keeps clearing it.
@@ -8817,7 +8833,7 @@ static void chatComposerPlaceButtons(LvChatPanel* p) {
const lv_coord_t b = CHAT_COMP_BTN_P4;
const lv_coord_t gap = 6;
const lv_coord_t right = chatComposerSendInset();
const lv_coord_t ta_h = s_comp_h - 4 - chatComposerTopRowH();
const lv_coord_t ta_h = s_comp_h - chatComposerChromeH();
const lv_coord_t kMinLift = SB_TOP_PAD - 2;
lv_coord_t lift = (ta_h - b) / 2;
if (lift < kMinLift) lift = kMinLift;
@@ -8844,6 +8860,7 @@ static void chatComposerApplyRows(LvChatPanel* p) {
lv_obj_set_size(p->send_btn, b, b);
lv_obj_set_style_radius(p->qr_btn, b / 2, LV_PART_MAIN);
lv_obj_set_style_radius(p->emoji_btn, b / 2, LV_PART_MAIN);
lv_obj_set_style_pad_bottom(p->composer_row, 2 + chatComposerBottomLift(), LV_PART_MAIN);
// Two-row: the top row's left end is free; the near-limit counter moves there so
// it doesn't sit on the emoji button.
if (p->composer_cnt) {
@@ -10692,7 +10709,10 @@ static void openEmojiPicker(lv_obj_t* ta, const char* const* items = k_emoji_ite
lv_obj_set_pos(title, 2, 0);
addCloseXBadge(card, emojiSheetCloseCb);
// Hint line: how to use the board's primary picker controls.
// Hint line: how to use the board's primary picker controls. The T-Deck Pro has no
// trackball or arrow keys to drive the selector (picks are by touch), so none there.
lv_coord_t grid_y = 26;
#if !defined(HAS_TDECK_PRO)
lv_obj_t* hint = lv_label_create(card);
#if defined(HAS_M9_KEYBOARD)
lv_label_set_text(hint, TR("Arrows \xE2\x80\xA2 OK"));
@@ -10701,14 +10721,31 @@ static void openEmojiPicker(lv_obj_t* ta, const char* const* items = k_emoji_ite
#endif
lv_obj_set_style_text_color(hint, lv_color_hex(COLOR_SUB), LV_PART_MAIN);
lv_obj_set_style_text_font(hint, &g_font_12, LV_PART_MAIN);
lv_obj_align(hint, LV_ALIGN_TOP_RIGHT, -24, 4);
// Title and hint share the top line when they fit left of the close badge; a narrow
// card gives the hint its own line under the title instead.
{
const char* tt = lv_label_get_text(title);
const char* ht = lv_label_get_text(hint);
const lv_coord_t title_w = lv_txt_get_width(tt, strlen(tt), &g_font_14, 0, LV_TEXT_FLAG_NONE);
const lv_coord_t hint_w = lv_txt_get_width(ht, strlen(ht), &g_font_12, 0, LV_TEXT_FLAG_NONE);
if (2 + title_w + 8 + hint_w + 24 <= cardw - 16) {
lv_obj_align(hint, LV_ALIGN_TOP_RIGHT, -24, 4);
} else {
lv_obj_set_width(hint, cardw - 16 - 2 - 24);
lv_label_set_long_mode(hint, LV_LABEL_LONG_DOT);
const lv_coord_t hy = lv_font_get_line_height(&g_font_14) + 2;
lv_obj_set_pos(hint, 2, hy);
grid_y = hy + lv_font_get_line_height(&g_font_12) + 6;
}
}
#endif
// Scrollable grid of glyph buttons.
const lv_coord_t grid_w = cardw - 16;
lv_obj_t* grid = lv_obj_create(card);
lv_obj_remove_style_all(grid);
lv_obj_set_size(grid, grid_w, cardh - 16 - 26);
lv_obj_set_pos(grid, 0, 26);
lv_obj_set_size(grid, grid_w, cardh - 16 - grid_y);
lv_obj_set_pos(grid, 0, grid_y);
lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_style_pad_row(grid, 4, LV_PART_MAIN);
lv_obj_set_style_pad_column(grid, 4, LV_PART_MAIN);
@@ -12707,9 +12744,9 @@ static void openAdvertPage() {
statusBarSetTall(true);
updateGlobalStatusBar();
#if CAP_ROUND_CORNERS
// The round panel's status bar is already its full two rows and never doubles when
// "tall", so the extra STATUSBAR_H below only left an empty band between the bar
// and the Home button. The root already starts below the bar.
// The round panel's status bar is one row and never doubles when "tall", so the
// extra STATUSBAR_H below only left an empty band between the bar and the Home
// button. The root already starts below the bar.
const int top = 8;
#else
const int top = STATUSBAR_H + 8; // clear the tall bar's second row, which overlaps the root
@@ -38828,8 +38865,9 @@ static void makeChatDetail(LvChatPanel& p) {
lv_obj_set_style_pad_all(p.msgs, 6, LV_PART_MAIN);
// 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) || defined(HELTEC_LORA_V4_R8)
// so retaining that old row-sized inset only wastes message space. The round
// panel's chat content already starts below its whole header (chatContentTop).
#if defined(TLORA_PAGER) || defined(HAS_TDECK_PRO) || defined(HELTEC_LORA_V4_R8) || CAP_ROUND_CORNERS
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);
@@ -38954,15 +38992,11 @@ static void makeChatDetail(LvChatPanel& p) {
const bool has_symbol_chip = chatHasSymbolChip(p.channel_mode);
const lv_coord_t send_sz = chatComposerSendSz();
const lv_coord_t comp_ta_w = chatComposerTaW(p.channel_mode);
const lv_coord_t comp_ta_x = has_symbol_chip
? chip_sz + chip_gap
: 2 * chip_sz + 2 * chip_gap;
const lv_coord_t symbol_x = has_symbol_chip
? comp_ta_x + comp_ta_w + chip_gap
: 0;
const lv_coord_t symbol_x = has_symbol_chip ? chip_sz + chip_gap : 0;
const lv_coord_t emoji_x = has_symbol_chip
? symbol_x + chip_sz + chip_gap
: chip_sz + chip_gap;
const lv_coord_t comp_ta_x = emoji_x + chip_sz + chip_gap;
// Macro picker button: a compact chip taps to a popup grid of user-
// defined quick-reply presets. Saves typing for stock phrases ("ok",
@@ -39053,9 +39087,9 @@ static void makeChatDetail(LvChatPanel& p) {
#endif
p.composer_ta = lv_textarea_create(p.composer_row);
// M9 conversations: QR | message input | # | emoji | Send. Other boards
// M9 conversations: QR | # | emoji | message input | Send. Other boards
// retain QR | emoji | message input | Send.
lv_obj_set_size(p.composer_ta, comp_ta_w, composer_h - 4 - chatComposerTopRowH());
lv_obj_set_size(p.composer_ta, comp_ta_w, composer_h - chatComposerChromeH());
// Bottom-aligned so the box grows UPWARD as the message wraps to more lines.
lv_obj_align(p.composer_ta, LV_ALIGN_BOTTOM_LEFT, comp_ta_x, 0);
styleCard(p.composer_ta);
@@ -48354,6 +48388,21 @@ static bool m9ChatMoveMessage(bool down) {
return true;
}
// Move d-pad focus to one of the chat composer's controls (# / emoji / text box).
// If it isn't in the nav group yet, queue it for the next group rebuild.
static void m9FocusChatControl(lv_obj_t* obj, bool edit) {
if (lv_obj_get_group(obj) == s_nav_group) {
s_m9_focus_pending = nullptr;
lv_group_focus_obj(obj);
} else {
s_m9_focus_pending = obj;
navMarkDirty();
navMaybeRebuild();
}
s_nav_ta_editing = edit;
s_nav_show = !edit;
}
static bool m9HandleArrowKey(int key, lv_obj_t* ta) {
if (!s_kbd_nav) return false;
// An open dropdown LIST owns the arrows (mirrors Tanmatsu's navPump capture):
@@ -48492,6 +48541,22 @@ static bool m9HandleArrowKey(int key, lv_obj_t* ta) {
return true;
}
}
// Chat composer (QR | # | emoji | text | Send): LEFT walks text → emoji → #.
if (LvChatPanel* chat = navOpenChatPanel();
chat && chat->composer_ta && chat->symbol_btn && chat->emoji_btn &&
lv_obj_is_valid(chat->symbol_btn) && lv_obj_is_valid(chat->emoji_btn)) {
lv_obj_t* focused = s_nav_group ? lv_group_get_focused(s_nav_group) : nullptr;
if (focused == chat->emoji_btn || s_m9_focus_pending == chat->emoji_btn) {
m9FocusChatControl(chat->symbol_btn, false);
if (g_lv.task) g_lv.task->noteUserInput();
return true;
}
if (ta == chat->composer_ta && lv_textarea_get_cursor_pos(ta) == 0) {
m9FocusChatControl(chat->emoji_btn, false);
if (g_lv.task) g_lv.task->noteUserInput();
return true;
}
}
if (ta) {
// Caret already at the start: fall through to focus-move so arrows
// always eventually LEAVE the field (a silent boundary no-op read as
@@ -48546,33 +48611,20 @@ static bool m9HandleArrowKey(int key, lv_obj_t* ta) {
navMarkDirty();
navMaybeRebuild();
}
} else if (LvChatPanel* chat = navOpenChatPanel();
chat && chat->symbol_btn && lv_obj_is_valid(chat->symbol_btn) &&
(ta == chat->composer_ta || navFocusedTextarea() == chat->composer_ta)) {
s_nav_ta_editing = false;
if (lv_obj_get_group(chat->symbol_btn) == s_nav_group) {
s_m9_focus_pending = nullptr;
s_nav_show = true;
lv_group_focus_obj(chat->symbol_btn);
} else {
s_m9_focus_pending = chat->symbol_btn;
navMarkDirty();
navMaybeRebuild();
}
} else if (LvChatPanel* chat = navOpenChatPanel();
chat && chat->symbol_btn && chat->emoji_btn &&
lv_obj_is_valid(chat->symbol_btn) && lv_obj_is_valid(chat->emoji_btn) &&
((s_nav_group && lv_group_get_focused(s_nav_group) == chat->symbol_btn) ||
s_m9_focus_pending == chat->symbol_btn)) {
if (lv_obj_get_group(chat->emoji_btn) == s_nav_group) {
s_m9_focus_pending = nullptr;
s_nav_show = true;
lv_group_focus_obj(chat->emoji_btn);
} else {
s_m9_focus_pending = chat->emoji_btn;
navMarkDirty();
navMaybeRebuild();
}
m9FocusChatControl(chat->emoji_btn, false);
} else if (LvChatPanel* chat = navOpenChatPanel();
chat && chat->emoji_btn && chat->composer_ta &&
lv_obj_is_valid(chat->emoji_btn) && lv_obj_is_valid(chat->composer_ta) &&
((s_nav_group && lv_group_get_focused(s_nav_group) == chat->emoji_btn) ||
s_m9_focus_pending == chat->emoji_btn)) {
m9FocusChatControl(chat->composer_ta, true);
if (g_lv.task) g_lv.task->noteUserInput();
return true;
} else if (ta) {
navMoveDir(NAV_RIGHT); // caret was already at end — same as LEFT
}
@@ -54524,10 +54576,10 @@ static void buildGlobalStatusBar() {
// LEFT-aligned group. ~70% of the double-bar height so there's breathing room
// above/below; soft rounded corners + a dimmer edge so they don't pop.
#if CAP_ROUND_CORNERS
// Round panel: the bar is a fixed two rows (no doubling), so size these to sit on
// ROW 2's left, inside the corner inset — the status cluster occupies row-2 right.
// Round panel: one row, so these sit on its left inside the corner inset (they are
// only ever shown on the Pager; kept clear of the clock line regardless).
const lv_coord_t BH = 18, BW = 30, GAP = 6, BX0 = SB_INSET_X;
const lv_coord_t BY = SB_TOP_PAD + SB_ROW + (SB_ROW - BH) / 2;
const lv_coord_t BY = SB_TOP_PAD + (SB_ROW - BH) / 2;
#elif defined(TLORA_PAGER)
// Match the compact status cluster instead of spanning most of the two-row
// header. Keep all three actions in the first status-bar row, sharing its
@@ -54860,7 +54912,6 @@ static void buildGlobalStatusBar() {
lv_color_hex(s_theme_high_contrast ? COLOR_SUB : COLOR_SECONDARY_ACTION), LV_PART_MAIN);
g_statusbar.sig_bars[i] = b;
}
#if !CAP_ROUND_CORNERS
// The SD activity light rides in the bars' empty top-left corner, above the
// shortest bar: a flicker of card I/O never moves the icons beside it (in its
// own slot it opened a gap the width of a slot between Bluetooth and Wi-Fi).
@@ -54870,7 +54921,6 @@ static void buildGlobalStatusBar() {
lv_obj_set_style_radius(g_statusbar.sd_icon, 2, LV_PART_MAIN);
lv_obj_align(g_statusbar.sd_icon, LV_ALIGN_TOP_LEFT, 0, 0); // replaces its right-middle anchor
}
#endif
}
// Keyboard layout indicator — sits left of the clock (only shown while typing).
@@ -54898,72 +54948,109 @@ static void buildGlobalStatusBar() {
lv_obj_add_flag(g_statusbar.dim, LV_OBJ_FLAG_HIDDEN);
#if CAP_ROUND_CORNERS
statusBarLayoutTwoRow(0); // place the clock (row 1) + the status cluster (row 2)
// No battery % or Bluetooth glyph on this panel (see statusBarLayoutOneRow).
if (g_statusbar.batt_pct) lv_obj_add_flag(g_statusbar.batt_pct, LV_OBJ_FLAG_HIDDEN);
if (g_statusbar.ble_icon) lv_obj_add_flag(g_statusbar.ble_icon, LV_OBJ_FLAG_HIDDEN);
statusBarLayoutOneRow();
#endif
}
#if CAP_ROUND_CORNERS
// Two-row status-bar layout for the round-cornered phone panel. Row 1 (top): the clock
// at the right inset (+ keyboard-layout tag to its left, + chat back/cog on the left).
// Row 2 (bottom): the battery pinned at the right inset, with the signal/wifi/sd/ble
// cluster to its left — that cluster slides right by `slide` px when the %-column is
// hidden while charging (mirrors the square-panel slide). The app-name / page-title /
// chat thread-name (left_label) is placed by updateGlobalStatusBar. Per-item y nudges
// centre the different glyph heights within their row. Base X offsets mirror the
// single-row builder so the horizontal spacing is unchanged; +SB_INSET_X keeps the end
// glyphs clear of the corner arcs.
// Right edge (px from the bar's right) of the row-2 sub-battery cluster: after the widest
// the percentage can get ("100%" in its font), not a fixed 64 px -- with the scaled font
// the signal box sat on top of "100%".
static bool s_crumb_capped = false; // round panel: breadcrumb width capped on row 2
static int statusClusterStart() {
const lv_font_t* pct_font = &g_font_12;
if (g_statusbar.batt_pct) pct_font = lv_obj_get_style_text_font(g_statusbar.batt_pct, LV_PART_MAIN);
const int pct_w = lv_txt_get_width("100%", 4, pct_font, 0, LV_TEXT_FLAG_NONE);
return LV_MAX(64, 26 + pct_w + 6);
}
// Left edge of the whole row-2 status cluster, measured from the bar's right edge.
static int statusClusterLeftExtent() { return statusClusterStart() + 62 + 14 + SB_INSET_X; }
// One-row status-bar layout for the round-cornered phone panel. Right to left from the
// corner inset: battery, signal bars (+ the SD light riding on them), Wi-Fi, the DND
// moon, then the clock. The device has Bluetooth hardware but wadamesh does not drive
// it there, so no Bluetooth glyph; the battery shows no %. The profile name / page
// title (left_label) sits on the same line at the left, placed by updateGlobalStatusBar.
static bool s_crumb_capped = false; // round panel: breadcrumb width capped short of the clock
static lv_coord_t s_sb_right_w = 0; // px from the bar's right edge to the clock's left edge
static lv_coord_t s_sb_line_bot = 0; // the shared bottom line (y) of the one-row bar
static void statusBarLayoutTwoRow(int slide) {
const int ins = SB_INSET_X;
// Row 2 — status cluster, right→left, evenly spaced. Battery pinned at the inset;
// the sub-battery cluster (signal/sd/wifi/ble) sits to its left and slides right by
// `slide` when the %-column hides while charging. y nudges centre each glyph in the
// row (montserrat_14 battery sits a touch higher than the 12px glyphs; sig/sd dots
// drop a few px). The scrolling profile name shares this row on the left (placed by
// updateGlobalStatusBar).
if (g_statusbar.batt_icon) lv_obj_align(g_statusbar.batt_icon, LV_ALIGN_TOP_RIGHT, -(2 + ins), SB_ROW2_Y - 2);
if (g_statusbar.batt_pct) lv_obj_align(g_statusbar.batt_pct, LV_ALIGN_TOP_RIGHT, -(26 + ins), SB_ROW2_Y);
const int cl = statusClusterStart();
// Charging hides the % column; the cluster then closes up to 32 px from the battery
// (the historical position), whatever width the % column had reserved.
if (slide) slide = cl - 32;
if (g_statusbar.sig_box) lv_obj_align(g_statusbar.sig_box, LV_ALIGN_TOP_RIGHT, -(cl + ins - slide), SB_ROW2_Y + 2);
if (g_statusbar.sd_icon) lv_obj_align(g_statusbar.sd_icon, LV_ALIGN_TOP_RIGHT, -(cl + 22 + ins - slide), SB_ROW2_Y + 4);
if (g_statusbar.conn_icon) lv_obj_align(g_statusbar.conn_icon, LV_ALIGN_TOP_RIGHT, -(cl + 38 + ins - slide), SB_ROW2_Y);
if (g_statusbar.ble_icon) lv_obj_align(g_statusbar.ble_icon, LV_ALIGN_TOP_RIGHT, -(cl + 62 + ins - slide), SB_ROW2_Y);
// Row 1 — clock at the right inset (+ keyboard-layout tag to its left when typing).
if (g_statusbar.clock) lv_obj_align(g_statusbar.clock, LV_ALIGN_TOP_RIGHT, -ins, SB_ROW1_Y);
if (g_statusbar.layout_label) lv_obj_align(g_statusbar.layout_label, LV_ALIGN_TOP_RIGHT, -(48 + ins), SB_ROW1_Y);
// Row 1 — chat back chevron + channel-settings cog on the left (shown only in a chat).
if (g_statusbar.chat_back) lv_obj_align(g_statusbar.chat_back, LV_ALIGN_TOP_LEFT, ins, SB_ROW1_Y);
if (g_statusbar.chan_gear) lv_obj_align(g_statusbar.chan_gear, LV_ALIGN_TOP_LEFT, ins + 24, SB_ROW1_Y);
}
#endif
#if CAP_ROUND_CORNERS
// Row 2 has only STATUSBAR_H - SB_ROW2_Y below its text top. At the Large/Huge
// UI sizes g_font_14 is taller than that, and the bar clipped the name's
// descenders (g, y, p). Give the label the largest font whose whole line fits.
static void statusBarFitRow2Font(lv_obj_t* l) {
const lv_coord_t room = STATUSBAR_H - SB_ROW2_Y;
const lv_font_t* const ladder[] = { &g_font_14, &g_font_12, &lv_font_montserrat_14, &lv_font_montserrat_12 };
const lv_font_t* pick = ladder[3];
for (const lv_font_t* f : ladder) {
if (lv_font_get_line_height(f) <= room) { pick = f; break; }
// Where a status glyph's ink starts and ends, measured down from its object's top.
// The glyphs mix fonts (16 px battery, 14 px Wi-Fi, 12 px moon), each with its own
// line box and baseline, so aligning the boxes leaves the drawn shapes at different
// heights. Labels use their first glyph; the signal bars are a plain box.
static void sbGlyphInk(lv_obj_t* o, lv_coord_t& top, lv_coord_t& bot) {
if (!lv_obj_check_type(o, &lv_label_class)) {
top = 0;
bot = lv_obj_get_style_height(o, LV_PART_MAIN);
return;
}
lv_obj_set_style_text_font(l, pick, LV_PART_MAIN);
const lv_font_t* f = lv_obj_get_style_text_font(o, LV_PART_MAIN);
const lv_coord_t base = lv_obj_get_style_pad_top(o, LV_PART_MAIN) + lv_font_get_line_height(f) - f->base_line;
top = bot = base;
const char* t = lv_label_get_text(o);
if (!t || !t[0]) return;
uint32_t i = 0;
const uint32_t c = _lv_txt_encoded_next(t, &i);
lv_font_glyph_dsc_t g;
if (!lv_font_get_glyph_dsc(f, &g, c, 0)) return;
bot = base - g.ofs_y;
top = bot - g.box_h;
}
// A text label's baseline, measured down from its object's top.
static lv_coord_t sbBaseline(lv_obj_t* o) {
const lv_font_t* f = lv_obj_get_style_text_font(o, LV_PART_MAIN);
return lv_obj_get_style_pad_top(o, LV_PART_MAIN) + lv_font_get_line_height(f) - f->base_line;
}
// Width of a status object for its current text (valid before the next layout pass).
static lv_coord_t sbObjW(lv_obj_t* o) {
if (!lv_obj_check_type(o, &lv_label_class)) return lv_obj_get_style_width(o, LV_PART_MAIN);
const char* t = lv_label_get_text(o);
return lv_txt_get_width(t, strlen(t), lv_obj_get_style_text_font(o, LV_PART_MAIN), 0, LV_TEXT_FLAG_NONE)
+ lv_obj_get_style_pad_left(o, LV_PART_MAIN) + lv_obj_get_style_pad_right(o, LV_PART_MAIN);
}
// Place o with its right edge `right` px in from the bar's right and its ink bottom on y = bottom.
static void sbAlignBottom(lv_obj_t* o, lv_coord_t right, lv_coord_t bottom) {
lv_coord_t t, b;
sbGlyphInk(o, t, b);
lv_obj_align(o, LV_ALIGN_TOP_RIGHT, -right, bottom - b);
}
// Pack the right-hand glyphs leftwards from x (px in from the bar's right), every one's
// ink bottom on one line chosen so the tallest shown is centred below the arc pad.
// Returns that line; x comes back at the left edge of the last glyph placed.
static lv_coord_t statusBarPackRound(bool links, bool dnd, lv_coord_t& x) {
struct Slot { lv_obj_t* o; lv_coord_t gap; bool show; };
const Slot order[] = {
{ g_statusbar.batt_icon, 0, true },
{ g_statusbar.sig_box, SBX(8), links },
{ g_statusbar.conn_icon, SBX(7), links },
{ g_statusbar.dnd_icon, SBX(7), dnd },
};
auto shown = [](const Slot& sl) {
if (!sl.show || !sl.o || lv_obj_has_flag(sl.o, LV_OBJ_FLAG_HIDDEN)) return false;
return !lv_obj_check_type(sl.o, &lv_label_class) || lv_label_get_text(sl.o)[0] != '\0';
};
lv_coord_t ink_h = 0;
for (const Slot& sl : order) {
if (!shown(sl)) continue;
lv_coord_t t, b;
sbGlyphInk(sl.o, t, b);
ink_h = LV_MAX(ink_h, b - t);
}
const lv_coord_t bot = (SB_TOP_PAD + statusBarCurH()) / 2 + ink_h / 2;
bool first = true;
for (const Slot& sl : order) {
if (!shown(sl)) continue;
if (!first) x += sl.gap;
sbAlignBottom(sl.o, x, bot);
x += sbObjW(sl.o);
first = false;
}
return bot;
}
// The whole one-row bar's right side: the glyphs, then the clock on the same line.
static void statusBarLayoutOneRow() {
lv_coord_t x = SB_INSET_X + 2;
const lv_coord_t bot = statusBarPackRound(true, true, x);
if (g_statusbar.clock && !lv_obj_has_flag(g_statusbar.clock, LV_OBJ_FLAG_HIDDEN)) {
x += 10;
lv_obj_align(g_statusbar.clock, LV_ALIGN_TOP_RIGHT, -x, bot - sbBaseline(g_statusbar.clock));
x += sbObjW(g_statusbar.clock);
if (g_statusbar.async_icon)
lv_obj_align_to(g_statusbar.async_icon, g_statusbar.clock, LV_ALIGN_OUT_LEFT_MID, -4, 0);
}
s_sb_right_w = x;
s_sb_line_bot = bot;
}
#endif
@@ -55038,12 +55125,34 @@ static void uiFitTrailingDots(const char* src, const lv_font_t* f, lv_coord_t ma
static void chatHeaderApply(bool chat_open) {
if (!g_statusbar.chat_avatar) return;
#if CAP_ROUND_CORNERS
// Round P4 panel: inset from the corner arcs and centred below the top-arc pad.
// Portrait drops the connectivity glyphs so the header fits one line; landscape has
// the room to keep Wi-Fi / signal beside the battery (never Bluetooth or a %).
const bool keep_links = chatLandscape();
const lv_coord_t inset = SB_INSET_X, ymid = SB_TOP_PAD / 2;
#else
const bool keep_links = false;
const lv_coord_t inset = 0, ymid = 0;
#endif
lv_obj_t* radios[] = { g_statusbar.conn_icon, g_statusbar.ble_icon, g_statusbar.sd_icon,
g_statusbar.dnd_icon, g_statusbar.sig_box, g_statusbar.sleep_icon };
auto isLink = [](lv_obj_t* o) {
return o == g_statusbar.conn_icon || o == g_statusbar.ble_icon || o == g_statusbar.sig_box;
};
static int s_radio_state = -1;
const int radio_state = chat_open ? (keep_links ? 2 : 1) : 0;
if (radio_state != s_radio_state) {
s_radio_state = radio_state;
for (lv_obj_t* o : radios) {
if (!o) continue;
const bool hide = chat_open && !(keep_links && isLink(o));
lv_obj_set_style_opa(o, hide ? LV_OPA_TRANSP : LV_OPA_COVER, LV_PART_MAIN);
}
}
static bool s_was_open = false;
if (chat_open != s_was_open) {
s_was_open = chat_open;
for (lv_obj_t* o : radios) if (o) lv_obj_set_style_opa(o, chat_open ? LV_OPA_TRANSP : LV_OPA_COVER, LV_PART_MAIN);
if (chat_open) {
lv_obj_clear_flag(g_statusbar.chat_avatar, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(g_statusbar.chat_sub, LV_OBJ_FLAG_HIDDEN);
@@ -55053,8 +55162,15 @@ static void chatHeaderApply(bool chat_open) {
lv_obj_add_flag(g_statusbar.chat_sub, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(g_statusbar.chat_head, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_height(g_statusbar.left_label, LV_SIZE_CONTENT); // the title's one-line box (below)
#if CAP_ROUND_CORNERS
// Hand the one-row bar its own layout back (the round bar never translates).
lv_obj_set_width(g_statusbar.left_label, LV_SIZE_CONTENT);
lv_label_set_long_mode(g_statusbar.left_label, LV_LABEL_LONG_WRAP);
statusBarLayoutOneRow();
#else
for (lv_obj_t* o : { g_statusbar.clock, g_statusbar.batt_icon, g_statusbar.batt_pct })
if (o) lv_obj_set_style_translate_y(o, -(lv_coord_t)(s_statusbar_tall ? STATUSBAR_H / 2 : 0), LV_PART_MAIN);
#endif
return;
}
}
@@ -55095,19 +55211,36 @@ static void chatHeaderApply(bool chat_open) {
// Layout across the full height of the tall bar.
const lv_coord_t bar_h = statusBarCurH();
const lv_coord_t av_sz = lv_obj_get_style_width(g_statusbar.chat_avatar, LV_PART_MAIN);
const lv_coord_t x_av = SC(24), x_text = x_av + av_sz + 8;
const lv_coord_t x_back = inset ? inset : 6;
const lv_coord_t x_av = x_back + SC(24) - 6, x_text = x_av + av_sz + 8;
lv_obj_set_style_translate_y(g_statusbar.chat_avatar, 0, LV_PART_MAIN);
lv_obj_align(g_statusbar.chat_avatar, LV_ALIGN_LEFT_MID, x_av, 0);
lv_obj_align(g_statusbar.chat_avatar, LV_ALIGN_LEFT_MID, x_av, ymid);
if (g_statusbar.chat_back) {
lv_obj_set_style_text_color(g_statusbar.chat_back, lv_color_hex(COLOR_TEXT), LV_PART_MAIN);
lv_obj_align(g_statusbar.chat_back, LV_ALIGN_LEFT_MID, 6, 0);
lv_obj_align(g_statusbar.chat_back, LV_ALIGN_LEFT_MID, x_back, ymid);
}
// Right side: battery, then the clock just left of it, centred on the bar.
for (lv_obj_t* o : { g_statusbar.clock, g_statusbar.batt_icon, g_statusbar.batt_pct })
if (o) lv_obj_set_style_translate_y(o, 0, LV_PART_MAIN);
#if CAP_ROUND_CORNERS
// The one-row bar's layout is re-packed here on the taller header's centre line:
// battery, then (landscape) signal and Wi-Fi.
lv_coord_t pct_w = inset + 2;
const lv_coord_t head_bot = statusBarPackRound(keep_links, false, pct_w);
#else
const char* pct = g_statusbar.batt_pct ? lv_label_get_text(g_statusbar.batt_pct) : "";
const lv_coord_t pct_w = (pct && pct[0]) ? lv_txt_get_width(pct, strlen(pct), &g_font_12, 0, LV_TEXT_FLAG_NONE) + SBX(22) : SBX(20);
lv_obj_align(g_statusbar.clock, LV_ALIGN_RIGHT_MID, -(pct_w + 8), 0);
#endif
#if CAP_ROUND_CORNERS
// The clock's digits sit on the same bottom line as the icons beside it.
{
lv_obj_align(g_statusbar.clock, LV_ALIGN_TOP_RIGHT, -(pct_w + 8), head_bot - sbBaseline(g_statusbar.clock));
}
if (g_statusbar.async_icon)
lv_obj_align_to(g_statusbar.async_icon, g_statusbar.clock, LV_ALIGN_OUT_LEFT_MID, -4, 0);
#else
lv_obj_align(g_statusbar.clock, LV_ALIGN_RIGHT_MID, -(pct_w + 8), ymid);
#endif
const char* clk = lv_label_get_text(g_statusbar.clock);
const lv_coord_t clk_w = lv_txt_get_width(clk, strlen(clk), lv_obj_get_style_text_font(g_statusbar.clock, LV_PART_MAIN), 0, LV_TEXT_FLAG_NONE);
const lv_coord_t text_w = lv_obj_get_style_width(g_statusbar.root, LV_PART_MAIN) - x_text - (pct_w + 8 + clk_w + 10);
@@ -55125,15 +55258,15 @@ static void chatHeaderApply(bool chat_open) {
uiFitTrailingDots(s_chat_title, &g_font_semi_14, LV_MAX((lv_coord_t)40, text_w), ttl, sizeof ttl);
if (strcmp(lv_label_get_text(g_statusbar.left_label), ttl) != 0) lv_label_set_text(g_statusbar.left_label, ttl);
}
const lv_coord_t top = (bar_h - title_h - sub_h) / 2;
const lv_coord_t top = (bar_h - title_h - sub_h) / 2 + ymid;
lv_obj_align(g_statusbar.left_label, LV_ALIGN_TOP_LEFT, x_text, top);
lv_obj_set_style_translate_y(g_statusbar.chat_sub, 0, LV_PART_MAIN);
lv_obj_set_size(g_statusbar.chat_sub, LV_MAX((lv_coord_t)40, text_w), sub_h);
lv_obj_align(g_statusbar.chat_sub, LV_ALIGN_TOP_LEFT, x_text, top + title_h);
// The hit area for chat settings: avatar and both lines.
lv_obj_set_style_translate_y(g_statusbar.chat_head, 0, LV_PART_MAIN);
lv_obj_set_size(g_statusbar.chat_head, x_text - x_av + LV_MAX((lv_coord_t)40, text_w), bar_h);
lv_obj_align(g_statusbar.chat_head, LV_ALIGN_LEFT_MID, x_av, 0);
lv_obj_set_size(g_statusbar.chat_head, x_text - x_av + LV_MAX((lv_coord_t)40, text_w), bar_h - 2 * ymid);
lv_obj_align(g_statusbar.chat_head, LV_ALIGN_LEFT_MID, x_av, ymid);
}
// The right-hand indicators, packed leftwards from the battery with one gap between
@@ -55141,7 +55274,7 @@ static void chatHeaderApply(bool chat_open) {
// hole. Fixed slots left the Bluetooth glyph a slot and more away from the Wi-Fi.
// Right to left: the battery, its %, the signal bars, Wi-Fi, Bluetooth, the DND moon,
// the keyboard-layout tag. updateGlobalStatusBar runs it only when one of them
// appears, goes or changes width. The round two-row bar has its own layout.
// appears, goes or changes width. The round one-row bar has its own (statusBarLayoutOneRow).
static uint32_t s_sb_pack_sig = 0; // what the last pack saw; the clock's overlap guard follows it
static void statusBarPackRight() {
if (!g_statusbar.root) return;
@@ -55274,6 +55407,13 @@ static void updateGlobalStatusBar() {
const bool want_tall = (s_settings_open_cat >= 0) || (s_apppage_title && !s_apppage_slim) || chat_open;
#endif
if (want_tall != s_statusbar_tall) statusBarSetTall(want_tall);
#if CAP_ROUND_CORNERS
// One row everywhere except an open chat, whose title + subtitle header is taller.
if (chat_open != s_round_chat_bar) {
s_round_chat_bar = chat_open;
lv_obj_set_height(g_statusbar.root, statusBarCurH());
}
#endif
// Glass lower row on double-height bars (settings detail, inbox/chat overview,
// open chat) so the tall bar looks consistent everywhere it appears. Switch the
// root's own fill transparent so only the fade backdrop paints the bar, then show
@@ -55281,7 +55421,7 @@ static void updateGlobalStatusBar() {
// built state — opaque root + hidden fade — already matches the inactive case).
// EXCEPT while the channel-settings / blocked-users sheets are open: those are a new
// page over the chat, so the bar goes SOLID (no glass revealing the chat behind it).
// The round panel keeps a solid two-row bar because there is no doubled lower row;
// The round panel keeps a solid bar because there is no doubled lower row;
// T-Deck Pro also stays solid because its monochrome e-paper cannot render glass.
#if defined(HAS_TDECK_PRO)
const bool fade_active = false;
@@ -55312,8 +55452,8 @@ static void updateGlobalStatusBar() {
// Per-element vertical placement in the tall bar. Default = TOP row (system content,
// same place as the regular bar). Exceptions: the settings back+title stays CENTRED;
// an open chat's thread name drops to the LOWER row and its cog centres across both.
// On the round panel the two rows come from each child's BASE align (set by
// statusBarLayoutTwoRow), not from a per-tick shift — so never translate there.
// The round panel's one row comes from each child's BASE align (set by
// statusBarLayoutOneRow), not from a per-tick shift — so never translate there.
const lv_coord_t up = (s_statusbar_tall && !CAP_ROUND_CORNERS) ? -(lv_coord_t)(STATUSBAR_H / 2) : 0;
const uint32_t nch = lv_obj_get_child_cnt(g_statusbar.root);
for (uint32_t i = 0; i < nch; ++i) {
@@ -55356,12 +55496,10 @@ static void updateGlobalStatusBar() {
// In a chat the thread name (+ unread prefix) is CENTRED on the lower row (the back +
// cog sit at the far left); otherwise it's left-aligned per the usual layout.
#if CAP_ROUND_CORNERS
// Round panel placement of the left zone:
// - open chat: thread name centred on ROW 1 (back/cog on row-1 left, clock row-1 right).
// - settings/tool page: "‹ Title" on ROW 2 left, its top level with the battery
// readout on the right (and clear of the clock on row 1).
// - home / other tabs: the scrolling profile name (or ✉ unread badge) sits on ROW 2,
// the SAME line as the battery/status cluster (per request), left-aligned.
// Round panel placement of the left zone (an open chat is laid out by chatHeaderApply):
// the "‹ Title" of a settings/tool page, or the scrolling profile name (or ✉ unread
// badge), sits at the left on the bar's one line, its baseline on the icons' bottom
// line, capped short of the clock.
if (s_crumb_capped && !(s_settings_open_cat >= 0 || s_apppage_title)) {
// Leaving a settings/tool page: drop the breadcrumb's width cap (the home tab's
// marquee config, applied further down, sets its own when it is the home tab).
@@ -55369,17 +55507,16 @@ static void updateGlobalStatusBar() {
lv_label_set_long_mode(g_statusbar.left_label, LV_LABEL_LONG_WRAP);
s_crumb_capped = false;
}
if (in_chan_chat) {
lv_obj_align(g_statusbar.left_label, LV_ALIGN_TOP_MID, 0, SB_ROW1_Y);
} else if (s_settings_open_cat >= 0 || s_apppage_title) {
lv_obj_align(g_statusbar.left_label, LV_ALIGN_TOP_LEFT, SB_INSET_X, SB_ROW2_Y);
// Sharing row 2 with the status cluster: cap the title short of it (ellipsis).
const int crumb_w = lv_obj_get_width(g_statusbar.root) - SB_INSET_X - statusClusterLeftExtent() - 6;
lv_obj_set_width(g_statusbar.left_label, LV_MAX(40, crumb_w));
lv_label_set_long_mode(g_statusbar.left_label, LV_LABEL_LONG_DOT);
s_crumb_capped = true;
} else {
lv_obj_align(g_statusbar.left_label, LV_ALIGN_TOP_LEFT, SB_INSET_X, SB_ROW2_Y);
if (!in_chan_chat) {
lv_obj_align(g_statusbar.left_label, LV_ALIGN_TOP_LEFT, SB_INSET_X,
s_sb_line_bot - sbBaseline(g_statusbar.left_label));
if (s_settings_open_cat >= 0 || s_apppage_title) {
// Sharing the row with the clock and icons: cap the title short of them (ellipsis).
const int crumb_w = lv_obj_get_width(g_statusbar.root) - SB_INSET_X - s_sb_right_w - 12;
lv_obj_set_width(g_statusbar.left_label, LV_MAX(40, crumb_w));
lv_label_set_long_mode(g_statusbar.left_label, LV_LABEL_LONG_DOT);
s_crumb_capped = true;
}
}
#else
if (in_chan_chat) {
@@ -55456,8 +55593,8 @@ static void updateGlobalStatusBar() {
// page title, CENTRED in the tall bar and a size up (tapping the bar goes Back). The
// chevron is tinted with the theme accent (the title text stays default).
#if CAP_ROUND_CORNERS
// Round panel: the title shares row 2 with the battery readout, so it uses that
// row's size rather than the tall bar's larger one.
// Round panel: the title shares the one row with the clock and icons, so it uses
// the row's size rather than the tall bar's larger one.
lv_obj_set_style_text_font(g_statusbar.left_label, &g_font_14, LV_PART_MAIN);
#else
lv_obj_set_style_text_font(g_statusbar.left_label,
@@ -55480,7 +55617,7 @@ static void updateGlobalStatusBar() {
// was removed to reclaim height). Keep the global unread badge as a prefix so
// you still see mail waiting in OTHER threads while reading this one. Smaller
// font so the name + badge fit.
#if defined(TLORA_PAGER) || CAP_ROUND_CORNERS
#if defined(TLORA_PAGER)
lv_obj_set_style_text_font(g_statusbar.left_label, &g_font_12, LV_PART_MAIN);
int total_unread = g_lv.task->getUnreadTotal();
if (total_unread > 0) {
@@ -55597,7 +55734,9 @@ static void updateGlobalStatusBar() {
} else {
lv_obj_add_flag(g_statusbar.conn_icon, LV_OBJ_FLAG_HIDDEN);
}
if (g_statusbar.ble_icon) {
// The P4 has Bluetooth hardware, but wadamesh does not support it there: its glyph
// stays hidden (from buildGlobalStatusBar on).
if (g_statusbar.ble_icon && !CAP_ROUND_CORNERS) {
if (ble_up) {
#if CAP_BLE_KEYBOARD
// Bluetooth is busy with a keyboard: say so instead of showing the radio.
@@ -55624,12 +55763,13 @@ static void updateGlobalStatusBar() {
// ---- Do Not Disturb icon ---- (shown only while the configured silence
// window is active — see dndActive()). On cramped bars (T-Display P4's
// round-corner two-row layout, or Heltec V4 rotated to portrait, <300px wide)
// round-corner layout, or Heltec V4 rotated to portrait, <300px wide)
// there's no room for a dedicated slot, so DND borrows the signal-bars slot
// instead while active; signal strength reclaims it the rest of the time.
{
// (The packed bars make room for the moon on their own, narrow ones included.)
const bool cramped = CAP_ROUND_CORNERS;
// (The round panel has room too, now that it shows no Bluetooth glyph or %.)
const bool cramped = false;
const bool active = dndActive();
const bool swap_signal = cramped && active;
@@ -55642,14 +55782,6 @@ static void updateGlobalStatusBar() {
else lv_obj_clear_flag(g_statusbar.sig_box, LV_OBJ_FLAG_HIDDEN);
}
if (g_statusbar.dnd_icon) {
#if CAP_ROUND_CORNERS
if (!swap_signal) {
// Dedicated slot — re-assert on entry/return so the moon self-heals from
// the borrowed signal-bars position instead of staying stranded there.
const int d = batteryIsCharging(batteryMvSmoothed()) ? 32 : 0;
lv_obj_align(g_statusbar.dnd_icon, LV_ALIGN_RIGHT_MID, -144 + d, 0);
}
#endif
if (active) lv_obj_clear_flag(g_statusbar.dnd_icon, LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(g_statusbar.dnd_icon, LV_OBJ_FLAG_HIDDEN);
}
@@ -55703,11 +55835,7 @@ static void updateGlobalStatusBar() {
lv_label_set_text(g_statusbar.batt_pct, buf);
if (charging != s_last_charging) {
// The % column disappears while charging (bolt only). The packed bars close
// up on their own (statusBarPackRight, run below on the change); the round
// two-row bar slides its row-2 cluster right by the hidden column instead.
#if CAP_ROUND_CORNERS
statusBarLayoutTwoRow(charging ? 32 : 0);
#endif
// up on their own (statusBarPackRight, run below on the change).
}
s_last_pct = pct;
s_last_charging = charging;
@@ -55826,11 +55954,9 @@ static void updateGlobalStatusBar() {
if (sig != s_clk_sig && visible(g_statusbar.clock)) {
s_clk_sig = sig;
#if CAP_ROUND_CORNERS
// Round P4 two-row bar: centre the clock on ROW 1 for the home / normal screens — row-1
// centre is free there (the node name lives on row 2). In a chat the row-1 centre is the
// thread title, so the clock moves to row-1 right instead.
if (chat_open) lv_obj_align(g_statusbar.clock, LV_ALIGN_TOP_RIGHT, -SB_INSET_X, SB_ROW1_Y);
else lv_obj_align(g_statusbar.clock, LV_ALIGN_TOP_MID, 0, SB_ROW1_Y);
// Round P4 one-row bar: the clock sits just left of the icons (statusBarLayoutOneRow);
// in a chat chatHeaderApply places it.
if (!chat_open) statusBarLayoutOneRow();
#else
// The redesign centres the clock on every board. (The block below placed it
// beside a node-name window that the bar no longer shows; the overlap guard
@@ -55867,7 +55993,7 @@ static void updateGlobalStatusBar() {
for (lv_obj_t* o : sb_icons) {
if (!visible(o)) continue;
lv_area_t ia; lv_obj_get_coords(o, &ia);
if (ia.y2 < ca.y1 || ia.y1 > ca.y2) continue; // another row (the round panel's two-row bar)
if (ia.y2 < ca.y1 || ia.y1 > ca.y2) continue; // another row
if (ia.x2 < ca.x1) continue; // left of the clock: not the right-hand cluster
if (ia.x1 < left_edge) left_edge = ia.x1;
if (ia.x1 < ca.x2 + kGap) overlap = true;
@@ -55905,7 +56031,28 @@ static void updateGlobalStatusBar() {
lv_obj_add_flag(g_statusbar.layout_label, LV_OBJ_FLAG_HIDDEN);
}
}
#if !defined(TLORA_PAGER) && !CAP_ROUND_CORNERS
#if CAP_ROUND_CORNERS
{
// The one row is packed by glyph ink and width: re-pack it when a glyph appears,
// goes or changes shape, or the clock's text changes width. An open chat's header
// is laid out by chatHeaderApply instead.
static uint32_t s_row_sig = 0;
uint32_t h = 2166136261u;
for (lv_obj_t* o : { g_statusbar.batt_icon, g_statusbar.conn_icon, g_statusbar.dnd_icon,
g_statusbar.sig_box, g_statusbar.clock }) {
h ^= (o && !lv_obj_has_flag(o, LV_OBJ_FLAG_HIDDEN)) ? 1u : 0u; h *= 16777619u;
const char* t = (o && lv_obj_check_type(o, &lv_label_class)) ? lv_label_get_text(o) : "";
for (const char* q = t; q && *q; ++q) { h ^= (uint8_t)*q; h *= 16777619u; }
h ^= 0xFF; h *= 16777619u;
}
h ^= chat_open ? 1u : 0u; h *= 16777619u;
if (h != s_row_sig) {
s_row_sig = h;
if (!chat_open) statusBarLayoutOneRow();
}
}
#endif
#if !defined(TLORA_PAGER)
chatHeaderApply((s_chat_title[0] != '\0') && !s_apppage_title && (s_settings_open_cat < 0));
#endif
}
@@ -57167,6 +57314,10 @@ static void themeStylesBuild() {
lv_style_set_bg_opa(&s_ts.kb_key_pr, LV_OPA_COVER);
lv_style_set_text_color(&s_ts.kb_key_pr, on_glow);
lv_style_set_bg_color(&s_ts.kb_key_chk, off_tone);
// Non-letter keys (Shift, Backspace, Enter, 1#, ...) are CHECKED items: label them
// like the letter keys, or they fall back to the base theme's dark text.
lv_style_set_text_color(&s_ts.kb_key_chk, text);
lv_style_set_text_font(&s_ts.kb_key_chk, &g_font_14);
// Arcs and spinners.
lv_style_set_arc_color(&s_ts.arc, border);
@@ -62822,7 +62973,7 @@ bool luaHostMeshSendChannel(const char* chan_name, const char* text) {
// since it is not flooded.
// The height an AppPage's title bar ACTUALLY occupies. The Lua host had this as
// a hardcoded 44, which is right only where STATUSBAR_H is its 22 default: it is
// SB_TOP_PAD + SB_ROW*2 on the round-corner phone panel, and SC(22) everywhere
// SB_TOP_PAD + SB_ROW on the round-corner phone panel, and SC(22) everywhere
// else, so it grows with the UI scale. Wherever it was taller than 44 the app's
// first line was drawn under the bar (#236 on the T-Display P4, and the same on
// any board at Large or Huge scale, which the Tanmatsu uses by default).
@@ -65956,7 +66107,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
// a resolved raw Montserrat for its chained twin, and every label that sets
// no font of its own calls it.
#if CAP_ROUND_CORNERS
STATUSBAR_H = SB_TOP_PAD + SB_ROW * 2; // top safe-area + two rows (round phone panel)
STATUSBAR_H = SB_TOP_PAD + SB_ROW; // top safe-area + one row (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) || defined(HAS_TDECK_GT911) || (defined(DOC_CAPTURE) && defined(DOC_VIRT_FONTSET))
+3 -2
View File
@@ -282,9 +282,10 @@
// Both can opt into the bounded, pre-transport saved-Wi-Fi sync from #383. The
// T-Display P4's PCF8563 has come back asserting impossible dates, so it gets the
// same fallback -- on the esp-hosted C6 build only: the sync drives Arduino's real
// WiFi, which the legacy ESP-AT build must never touch.
// WiFi, which the legacy ESP-AT build must never touch. The Wio Tracker L2 has
// no RTC and an ESP32-S3 radio, so it gets the same opt-in.
#if defined(HAS_TDECK_GT911) || defined(HAS_TDECK_PRO) || defined(HAS_THINKNODE_M9) || \
(defined(HAS_TDISPLAY_P4) && TDP4_C6_HOSTED)
defined(HAS_WIO_TRACKER_L2) || (defined(HAS_TDISPLAY_P4) && TDP4_C6_HOSTED)
#define CAP_BOOT_TIME_SYNC 1
#else
#define CAP_BOOT_TIME_SYNC 0