fix(ui-lvgl): tile badges in the corner, minimap without a fix, taller header, accent status icons

- Unread badge on Home tiles sits in the top-right corner, clear of the icon
- Minimap with no GPS fix: the saved position (hollow dot), else where the
  map was left, else the default view; a hint when no tiles cover the area
- Screen header 38 px, so the back and header buttons get a margin
- Bluetooth / WiFi connected shown in the accent colour; charging bolt green

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-27 01:41:46 +02:00
co-authored by Claude Opus 5.5
parent e52ec2a97e
commit 5289f6b11a
4 changed files with 47 additions and 17 deletions
@@ -477,7 +477,7 @@ void UITask::favGrid(lv_obj_t* grid) {
if (used) {
int unread = chan ? _core->history.chUnread(_prefs->favourite_contacts[s][0])
: (c.type == ADV_TYPE_CHAT ? _core->dmUnread(c.id.pub_key) : 0);
badge(b, unread, false);
tileBadge(b, unread);
}
}
lv_obj_t* hint = label(grid, "Tap to open. Hold to change or remove.", THEME_FONT_SMALL, theme::TEXT_MUTED);
+21 -7
View File
@@ -128,7 +128,7 @@ static lv_obj_t* homeTile(lv_obj_t* parent, const char* icon, const char* text,
lv_obj_set_style_text_align(n, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_width(n, w - 10);
lv_obj_align(n, LV_ALIGN_BOTTOM_MID, 0, -8);
badge(b, badge_n, false);
tileBadge(b, badge_n);
return b;
}
@@ -412,7 +412,12 @@ void UITask::homeMapFit() {
bool me = _core->course.currentLocation(lat, lon);
int live = 0;
bool target = false;
bool last = false; // no fix: the saved / app-set position, hollow
if (me) s_pts[s_npts++] = { lat, lon, dot(14, theme::ACCENT, theme::BG) };
else if (_sensors && (_sensors->node_lat != 0 || _sensors->node_lon != 0)) {
last = true;
s_pts[s_npts++] = { (int32_t)lround(_sensors->node_lat * 1e6), (int32_t)lround(_sensors->node_lon * 1e6), dot(14, theme::BG, theme::ACCENT) };
}
if (_core->locator.activeTargetPos(lat, lon)) {
target = true;
s_pts[s_npts++] = { lat, lon, dot(12, theme::BG, theme::ACCENT) };
@@ -429,15 +434,21 @@ void UITask::homeMapFit() {
lv_obj_move_foreground(s_caption);
char cap[48];
int o = snprintf(cap, sizeof(cap), "%s", me ? "You" : "No position yet");
int o = snprintf(cap, sizeof(cap), "%s", me ? "You" : last ? "Last position" : "No position yet");
if (live) o += snprintf(cap + o, sizeof(cap) - o, " " LV_SYMBOL_GPS " %d sharing", live);
if (target) snprintf(cap + o, sizeof(cap) - o, " " UI_SYMBOL_FLAG " target");
lv_label_set_text(s_caption, cap);
if (s_npts == 0) { // nothing to show: the default view
s_z = mapview::DEFAULT_Z;
s_cx = mapview::lonToTileX(mapview::DEFAULT_LON, s_z);
s_cy = mapview::latToTileY(mapview::DEFAULT_LAT, s_z);
if (s_npts == 0) { // nothing to show: where the map was left, else the default view
if (_map_z) {
s_z = _map_z;
s_cx = _map_cx;
s_cy = _map_cy;
} else {
s_z = mapview::DEFAULT_Z;
s_cx = mapview::lonToTileX(mapview::DEFAULT_LON, s_z);
s_cy = mapview::latToTileY(mapview::DEFAULT_LAT, s_z);
}
return;
}
// Bounds in world units (tiles at z 0), then the deepest zoom they fit at.
@@ -465,6 +476,7 @@ void UITask::homeMapLayout() {
int tx0 = (int)floor(left / mapview::TILE_PX), ty0 = (int)floor(top / mapview::TILE_PX);
int n = 1 << s_z;
s_pending = false;
int shown = 0;
for (int j = 0; j < ROWS; j++) {
for (int i = 0; i < COLS; i++) {
lv_obj_t* cell = s_cells[j * COLS + i];
@@ -492,6 +504,7 @@ void UITask::homeMapLayout() {
lv_obj_set_pos(img, -(wx & m) * mapview::TILE_PX, -(ty & m) * mapview::TILE_PX);
lv_obj_set_pos(cell, px, py);
lv_obj_remove_flag(cell, LV_OBJ_FLAG_HIDDEN);
shown++;
}
}
for (int i = 0; i < s_npts; i++) {
@@ -500,7 +513,8 @@ void UITask::homeMapLayout() {
int d = lv_obj_get_width(s_pts[i].obj);
lv_obj_set_pos(s_pts[i].obj, (int)lround(x) - d / 2, (int)lround(y) - d / 2);
}
const char* hint = !mapview::s_available ? "No map on the SD card" : nullptr;
const char* hint = !mapview::s_available ? "No map on the SD card"
: !shown && !s_pending ? "No map tiles for this area" : nullptr;
if (hint) { lv_label_set_text(s_hint, hint); lv_obj_remove_flag(s_hint, LV_OBJ_FLAG_HIDDEN); }
else lv_obj_add_flag(s_hint, LV_OBJ_FLAG_HIDDEN);
}
+24 -9
View File
@@ -655,6 +655,13 @@ static void badge(lv_obj_t* parent, int n, bool overflow) {
lv_obj_center(l);
}
// The same on a Home tile: in its top-right corner, clear of the icon.
static void tileBadge(lv_obj_t* tile, int n) {
if (n <= 0) return;
badge(tile, n, false);
lv_obj_align(lv_obj_get_child(tile, -1), LV_ALIGN_TOP_RIGHT, -2, 2);
}
// Local time (NodePrefs::tz_offset_hours); false before the clock is set.
static bool localTime(const NodePrefs* p, struct tm& out) {
uint32_t now = rtc_clock.getCurrentTime();
@@ -1095,6 +1102,8 @@ void UITask::buildStatusBar() {
lv_obj_align(_status_time, LV_ALIGN_LEFT_MID, 0, 0);
_status_batt = label(bar, "", THEME_FONT_ICONS, theme::TEXT_MUTED);
lv_obj_align(_status_batt, LV_ALIGN_RIGHT_MID, 0, 0);
_status_chg = label(bar, LV_SYMBOL_CHARGE, THEME_FONT_ICONS, theme::OK);
lv_obj_add_flag(_status_chg, LV_OBJ_FLAG_HIDDEN);
_status_icons = lv_obj_create(bar);
lv_obj_remove_style_all(_status_icons);
lv_obj_remove_flag(_status_icons, LV_OBJ_FLAG_CLICKABLE);
@@ -1146,19 +1155,25 @@ void UITask::refreshStatusBar() {
case battery::VOLTAGE: snprintf(level, sizeof(level), " %u.%02u V", mv / 1000, (mv % 1000) / 10); break;
default: break;
}
lv_label_set_text_fmt(_status_batt, "%s%s%s", _board->isExternalPowered() ? LV_SYMBOL_CHARGE : "", batt, level);
lv_label_set_text_fmt(_status_batt, "%s%s", batt, level);
lv_obj_t* left_of = _status_batt; // the icons pack up to this
if (_board->isExternalPowered()) {
lv_obj_remove_flag(_status_chg, LV_OBJ_FLAG_HIDDEN);
lv_obj_align_to(_status_chg, _status_batt, LV_ALIGN_OUT_LEFT_MID, -2, 0);
left_of = _status_chg;
} else lv_obj_add_flag(_status_chg, LV_OBJ_FLAG_HIDDEN);
// Status icons, as the original's status bar (ui-new): Bluetooth (bright
// when the app is connected), WiFi (when switched on), GPS (green with a fix), the alarm, mute, then
// Status icons, as the original's status bar (ui-new): Bluetooth (accent
// when the app is connected), WiFi (when switched on, accent connected), GPS (green with a fix), the alarm, mute, then
// the modes that keep running in the background -- auto-advert, trail, live
// share, repeater, arrival alert -- in the accent colour. Right to left in
// that order, next to the battery.
struct Icon { const char* sym; uint32_t col; };
Icon icons[11];
int n = 0;
if (isSerialEnabled()) icons[n++] = { LV_SYMBOL_BLUETOOTH, hasConnection() ? theme::TEXT : theme::TEXT_MUTED };
if (lvport::wifiAllowed()) // WiFi switched on: bright while connected (map tiles, update)
icons[n++] = { LV_SYMBOL_WIFI, lvport::netRadio() == lvport::NET_UP ? theme::TEXT : theme::TEXT_MUTED };
if (isSerialEnabled()) icons[n++] = { LV_SYMBOL_BLUETOOTH, hasConnection() ? theme::ACCENT : theme::TEXT_MUTED };
if (lvport::wifiAllowed()) // WiFi switched on: accent while connected (map tiles, update)
icons[n++] = { LV_SYMBOL_WIFI, lvport::netRadio() == lvport::NET_UP ? theme::ACCENT : theme::TEXT_MUTED };
int32_t lat, lon;
bool fix = _core->course.currentLocation(lat, lon);
if (_core->gpsEnabled() || fix) icons[n++] = { LV_SYMBOL_GPS, fix ? theme::OK : theme::TEXT_MUTED };
@@ -1181,7 +1196,7 @@ void UITask::refreshStatusBar() {
for (int i = n - 1; i >= 0; i--) // the icon font gives each the same size and cell
label(_status_icons, icons[i].sym, THEME_FONT_ICONS, icons[i].col);
}
lv_obj_align_to(_status_icons, _status_batt, LV_ALIGN_OUT_LEFT_MID, -5, 0);
lv_obj_align_to(_status_icons, left_of, LV_ALIGN_OUT_LEFT_MID, -5, 0);
}
void UITask::setGps(bool on) {
@@ -1256,7 +1271,7 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) {
lv_obj_t* hdr = lv_obj_create(scr);
styleSurface(hdr, theme::BG);
lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(hdr, LV_PCT(100), 32);
lv_obj_set_size(hdr, LV_PCT(100), 38);
lv_obj_set_pos(hdr, 0, top);
_header = hdr;
if (with_back) {
@@ -1272,7 +1287,7 @@ 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, 230);
lv_obj_align(t, LV_ALIGN_LEFT_MID, with_back ? 52 : theme::PAD, 0);
top += 32;
top += 38;
}
lv_obj_t* body = lv_obj_create(scr);
@@ -442,6 +442,7 @@ private:
lv_obj_t* _status_time = nullptr;
lv_obj_t* _status_icons = nullptr; // a row of icon labels, rebuilt with the state
lv_obj_t* _status_batt = nullptr;
lv_obj_t* _status_chg = nullptr; // the charging bolt, green, left of the battery
char _status_sig[128] = ""; // what the icon row shows, to skip unchanged rebuilds
lv_obj_t* _toast = nullptr;
lv_timer_t* _toast_timer = nullptr;