From 1683be63cabe58057b05370c0424c19f6cda6a35 Mon Sep 17 00:00:00 2001 From: MarekZegare4 Date: Mon, 19 Oct 2026 12:58:04 +0200 Subject: [PATCH] fix(l2): title bar fits its title - the battery level, then button words, give way Co-Authored-By: Claude Sonnet 5.5 --- examples/companion_radio/ui-lvgl/ChatList.h | 5 +- examples/companion_radio/ui-lvgl/UITask.cpp | 80 ++++++++++++++++++--- examples/companion_radio/ui-lvgl/UITask.h | 2 + 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/examples/companion_radio/ui-lvgl/ChatList.h b/examples/companion_radio/ui-lvgl/ChatList.h index 49439e4d..25ba9ce7 100644 --- a/examples/companion_radio/ui-lvgl/ChatList.h +++ b/examples/companion_radio/ui-lvgl/ChatList.h @@ -172,9 +172,8 @@ void UITask::buildChats() { lv_obj_t* body = newScreen("Messages", true); s_chats_read_all = unreadTotal() > 0; if (_header) { // a new message last, at the edge, whatever the lists below hold - // Icons alone: with "Read all" spelled out the title was squeezed to "Me..." - if (s_chats_read_all) headerButton(_header, LV_SYMBOL_OK, onMarkAllRead, 0, NULL); - stylePrimary(headerButton(_header, LV_SYMBOL_EDIT, onNewChat, 0, NULL)); // the pencil alone: the title fits + if (s_chats_read_all) headerButton(_header, LV_SYMBOL_OK " Read all", onMarkAllRead, 0, NULL); + stylePrimary(headerButton(_header, LV_SYMBOL_EDIT, onNewChat, 0, NULL)); } vlist::attach(body); fillChats(); diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index 781de892..c1575749 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -113,6 +113,7 @@ static const char* waypointsFull() { // piece of state it shows comes from the UI Core; this file only draws it. static UITask* s_ui = nullptr; // for LVGL's C callbacks +static bool s_hdr_fit; // the title bar's contents changed: fitHeader() before the next frame #if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__) #include @@ -179,7 +180,10 @@ void UITask::begin(DisplayDriver* display_drv, SensorManager* sensors, NodePrefs theme::setAccent(lvport::loadAccent()); theme::setLight(lvport::loadLight()); theme::install(lv_display_get_default()); - lv_display_add_event_cb(lv_display_get_default(), [](lv_event_t*) { rowsFitPending(); }, LV_EVENT_REFR_START, NULL); + lv_display_add_event_cb(lv_display_get_default(), [](lv_event_t*) { + rowsFitPending(); + if (s_hdr_fit && s_ui) { s_hdr_fit = false; s_ui->fitHeader(); } + }, LV_EVENT_REFR_START, NULL); buildStatusBar(); quickPanelBuild(); // QuickPanel.h: ready before the first pull @@ -699,6 +703,64 @@ static void placeLeftOf(lv_obj_t* o, lv_obj_t* base, int32_t dx) { lv_obj_align_to(o, base, LV_ALIGN_OUT_LEFT_MID, dx, 0); } +// Whether the title fits its bar. The bar is a row where the title takes what +// the rest leave, so the rest gives way, in this order: the battery's level +// (the icon stays), then the words of the screen's buttons (their icons stay, +// last button first). Only when that is not enough is the title cut with "...". +// A label with a space after its icon is collapsible: clipped to the icon's +// width, its text untouched, so a screen may rewrite it later. +static void collapseLabel(lv_obj_t* l, const lv_font_t* font, bool on) { + const char* t = lv_label_get_text(l); + const char* sp = strchr(t, ' '); + if (!on || !sp) { + lv_label_set_long_mode(l, LV_LABEL_LONG_WRAP); + lv_obj_set_width(l, LV_SIZE_CONTENT); + return; + } + char icon[8]; + size_t n = (size_t)(sp - t) < sizeof(icon) - 1 ? (size_t)(sp - t) : sizeof(icon) - 1; + memcpy(icon, t, n); + icon[n] = '\0'; + lv_point_t sz; + lv_text_get_size(&sz, icon, font, 0, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE); + lv_label_set_long_mode(l, LV_LABEL_LONG_CLIP); + lv_obj_set_width(l, sz.x); +} + +void UITask::fitHeader() { + if (!_header || !_hdr_clock || !_hdr_title) return; + lv_obj_t* lbl[8]; // the collapsible labels, in the row's order, the battery's apart + int n = 0; + const int cnt = (int)lv_obj_get_child_count(_header); + for (int i = 0; i < cnt && n < 8; i++) { + lv_obj_t* b = lv_obj_get_child(_header, i); + if (!lv_obj_check_type(b, &lv_button_class) || lv_obj_get_child_count(b) == 0) continue; + lv_obj_t* l = lv_obj_get_child(b, 0); + if (lv_obj_check_type(l, &lv_label_class)) lbl[n++] = l; + } + lv_point_t tsz; + lv_text_get_size(&tsz, lv_label_get_text(_hdr_title), THEME_FONT_TITLE, 0, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE); + auto place = [&]() { // the row stops short of the clock pill + layoutNow(_hdr_clock); + const int32_t w = lv_obj_get_width(_hdr_clock); + if (lv_obj_get_style_pad_right(_header, LV_PART_MAIN) != w) { + lv_obj_set_style_pad_right(_header, w, 0); + lv_obj_align(_hdr_clock, LV_ALIGN_RIGHT_MID, w, 0); // aligned inside the padding: out into it + } + layoutNow(_header); + return lv_obj_get_width(_hdr_title) >= tsz.x; + }; + collapseLabel(_hdr_batt, THEME_FONT_ICONS, false); + for (int i = 0; i < n; i++) collapseLabel(lbl[i], THEME_FONT_SMALL, false); + if (place()) return; + collapseLabel(_hdr_batt, THEME_FONT_ICONS, true); + if (place()) return; + for (int i = n - 1; i >= 0; i--) { + collapseLabel(lbl[i], THEME_FONT_SMALL, true); + if (place()) return; + } +} + void UITask::refreshStatusBar() { if (!_status_time) return; struct tm ti; @@ -725,15 +787,13 @@ void UITask::refreshStatusBar() { setTextFmt(_status_batt, "%s%s", batt, level); const bool chg = _board->isExternalPowered(); if (_hdr_clock) { // the title bar's: the time over the battery, green on the charger + char hb[40]; + snprintf(hb, sizeof(hb), "%s%s", batt, level); + // the pill's width changes with these: fitHeader() before the next frame + if (!labelShows(_hdr_time, lv_label_get_text(_status_time)) || !labelShows(_hdr_batt, hb)) s_hdr_fit = true; setText(_hdr_time, lv_label_get_text(_status_time)); - setTextFmt(_hdr_batt, "%s%s", batt, level); + setText(_hdr_batt, hb); setTextColor(_hdr_batt, chg ? theme::OK : theme::TEXT_MUTED); - layoutNow(_hdr_clock); - const int32_t w = lv_obj_get_width(_hdr_clock); // the row stops short of it - if (lv_obj_get_style_pad_right(_header, LV_PART_MAIN) != w) { - lv_obj_set_style_pad_right(_header, w, 0); - lv_obj_align(_hdr_clock, LV_ALIGN_RIGHT_MID, w, 0); // aligned inside the padding: out into it - } } lv_obj_t* left_of = _status_batt; // the icons pack up to this if (chg) { @@ -1047,7 +1107,7 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) { // and at the right end the time and battery. A drag down on it brings the // quick panel, as on the status bar. int top = theme::STATUS_H; - _hdr_clock = _hdr_time = _hdr_batt = nullptr; + _hdr_clock = _hdr_time = _hdr_batt = _hdr_title = nullptr; if (title) { lv_obj_t* hdr = lv_obj_create(scr); styleSurface(hdr, theme::BG); @@ -1075,6 +1135,8 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) { lv_label_set_long_mode(t, LV_LABEL_LONG_DOT); lv_obj_set_width(t, 1); lv_obj_set_flex_grow(t, 1); // what the actions leave + _hdr_title = t; + s_hdr_fit = true; // The time and battery on a pill as the bar's buttons are, out of the // row: the row stops short of it (the bar's right padding, refreshStatusBar()). lv_obj_t* c = lv_obj_create(hdr); diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index eb702638..6f1f0152 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -387,6 +387,7 @@ private: void buildStatusBar(); void refreshStatusBar(); + void fitHeader(); lv_obj_t* newScreen(const char* title, bool with_back); void showSplash(); // Splash.h // The screen being shown: newScreen()'s, already while it slides in (popups @@ -621,6 +622,7 @@ private: lv_obj_t* _home_unread = nullptr; lv_obj_t* _header = nullptr; // current screen's title bar (nullptr on Home) lv_obj_t* _hdr_clock = nullptr; // its time and battery, at the right end + lv_obj_t* _hdr_title = nullptr; // its title, which the battery's level gives way to lv_obj_t* _hdr_time = nullptr; lv_obj_t* _hdr_batt = nullptr; lv_obj_t* _body = nullptr;