From c779bf46b5c177368a0272b3cc441ae745483ffe Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:51:03 +0200 Subject: [PATCH] feat(ui-lvgl): title bar with the clock, tidier controls, contextual nav bar Density from less chrome instead of smaller rows (ROW_H stays 44): - titled screens drop the status bar; the title bar (APPBAR_H) holds back, the title, the screen's actions and the time/battery on a pill; the status bar returns for Home, the map, typing, the quick panel and the lock - one radius (RADIUS) for everything tapped: buttons, tabs, choices, chips - tabs are all segmented at TAB_H in the body (GPS, Clock converted) - Nodes: chips row + sort + status line become one toolbar row with Show / Order choices; Messages' All/Favourites is the same choice - nav map: the target bar only while there is a target; the pin opens one "Places" popup; back from a map options page restores the map and Map tools as left, without a jump - node detail actions at the top of the scroll; Home tiles fill the page - rowsFit() leaves growing labels to the flex (a screen title cut to one letter after Messages' filter changed) Co-Authored-By: Claude Opus 5.5 --- docs/solo/contacts.md | 5 + docs/solo/navigation.md | 11 +- .../companion_radio/ui-lvgl/AdminScreen.h | 2 +- examples/companion_radio/ui-lvgl/BotScreen.h | 4 +- examples/companion_radio/ui-lvgl/ChatList.h | 35 ++-- .../companion_radio/ui-lvgl/ClockScreen.h | 19 +- .../ui-lvgl/ConversationScreen.h | 5 +- .../companion_radio/ui-lvgl/DeviceScreen.h | 10 +- examples/companion_radio/ui-lvgl/DiagScreen.h | 2 +- examples/companion_radio/ui-lvgl/GpsScreen.h | 5 +- examples/companion_radio/ui-lvgl/HomeScreen.h | 10 +- examples/companion_radio/ui-lvgl/MapScreen.h | 39 +++- examples/companion_radio/ui-lvgl/NavMap.h | 179 ++++++++---------- examples/companion_radio/ui-lvgl/NodesView.h | 90 ++++----- examples/companion_radio/ui-lvgl/QuickPanel.h | 6 +- .../companion_radio/ui-lvgl/QuickScreen.h | 2 +- .../companion_radio/ui-lvgl/RouteScreen.h | 4 +- .../companion_radio/ui-lvgl/SoundScreen.h | 4 +- examples/companion_radio/ui-lvgl/Theme.h | 11 +- examples/companion_radio/ui-lvgl/ThreadView.h | 6 +- examples/companion_radio/ui-lvgl/UITask.cpp | 131 +++++++++++-- examples/companion_radio/ui-lvgl/UITask.h | 14 +- examples/companion_radio/ui-lvgl/Widgets.h | 58 +++++- 23 files changed, 408 insertions(+), 244 deletions(-) diff --git a/docs/solo/contacts.md b/docs/solo/contacts.md index 700f518c..1c423103 100644 --- a/docs/solo/contacts.md +++ b/docs/solo/contacts.md @@ -8,6 +8,11 @@ filters by type (All, Fav, Companions, Repeaters, Rooms, Sensors); the sort, by distance or by last heard, is in the options. A ★ marks a favourite, a ♦ someone sharing their position live. +> [!NOTE] +> **Wio Tracker L2:** the line over the list holds the count and two choices, +> which nodes to show and in what order; **Scan** and the map are in the +> title bar. + **Enter** shows a node's details. **Hold Enter**, in the list or the details, for what you can do with it: diff --git a/docs/solo/navigation.md b/docs/solo/navigation.md index 38dde92c..63f7b828 100644 --- a/docs/solo/navigation.md +++ b/docs/solo/navigation.md @@ -57,8 +57,10 @@ start**, so you can always go back to where the trail began. contact or channel) and **Set as target**. > [!NOTE] -> **Wio Tracker L2:** up to 64 waypoints. The pin button on the map opens the -> list; hold a spot on the map to make it a waypoint or the target. +> **Wio Tracker L2:** up to 64 waypoints. The pin button on the map opens +> **Places**: mark where you are, type coordinates, or tap a waypoint, the +> trail start or someone sharing live to navigate there. Hold a spot on the +> map to make it a waypoint or the target. ## Navigating to something @@ -73,8 +75,9 @@ degrees and direction. > [!NOTE] > **Wio Tracker L2:** the target is drawn on the map with a line to it, and a -> bar with distance, bearing, heading and time to arrival. The Compass app is -> a turning dial. +> bar along the bottom shows distance, bearing, heading and time to arrival +> (without a target the map has the whole screen). The Compass app is a +> turning dial. ## Sharing locations diff --git a/examples/companion_radio/ui-lvgl/AdminScreen.h b/examples/companion_radio/ui-lvgl/AdminScreen.h index ebdc0b79..b089dbfc 100644 --- a/examples/companion_radio/ui-lvgl/AdminScreen.h +++ b/examples/companion_radio/ui-lvgl/AdminScreen.h @@ -171,7 +171,7 @@ void UITask::buildAdmin() { static const char* TABS[admin::TAB_COUNT + 1]; for (int i = 0; i < admin::TAB_COUNT; i++) TABS[i] = admin::TAB_LABELS[i]; TABS[admin::TAB_COUNT] = ""; - s_tabs = segmented(body, TABS, s_tab, LV_PCT(100), 34, theme::SURFACE); + s_tabs = segmented(body, TABS, s_tab, LV_PCT(100), theme::TAB_H, theme::SURFACE); lv_obj_add_event_cb(s_tabs, onAdminTab, LV_EVENT_VALUE_CHANGED, NULL); s_status = label(body, "", THEME_FONT_SMALL, theme::ACCENT); diff --git a/examples/companion_radio/ui-lvgl/BotScreen.h b/examples/companion_radio/ui-lvgl/BotScreen.h index bb9d9074..2838d0c6 100644 --- a/examples/companion_radio/ui-lvgl/BotScreen.h +++ b/examples/companion_radio/ui-lvgl/BotScreen.h @@ -44,7 +44,7 @@ static void botChip(const char* ph, void* parent) { lv_obj_set_height(b, 26); lv_obj_set_style_pad_hor(b, 7, 0); lv_obj_set_style_pad_ver(b, 0, 0); - lv_obj_set_style_radius(b, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_radius(b, theme::RADIUS, 0); lv_obj_set_style_shadow_width(b, 0, 0); lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 0); lv_obj_add_event_cb(b, onBotPlaceholder, LV_EVENT_CLICKED, NULL); @@ -86,7 +86,7 @@ void UITask::buildBot() { static const char* TABS[botcfg::TAB_COUNT + 1]; for (int i = 0; i < botcfg::TAB_COUNT; i++) TABS[i] = botcfg::TAB_LABELS[i]; TABS[botcfg::TAB_COUNT] = ""; - lv_obj_t* tabs = segmented(body, TABS, s_tab, lv_pct(100), 34); + lv_obj_t* tabs = segmented(body, TABS, s_tab, lv_pct(100), theme::TAB_H); lv_obj_set_style_bg_color(tabs, lv_color_hex(theme::SURFACE), LV_PART_ITEMS); lv_obj_add_event_cb(tabs, onBotTab, LV_EVENT_VALUE_CHANGED, NULL); diff --git a/examples/companion_radio/ui-lvgl/ChatList.h b/examples/companion_radio/ui-lvgl/ChatList.h index 3b468e48..705754a5 100644 --- a/examples/companion_radio/ui-lvgl/ChatList.h +++ b/examples/companion_radio/ui-lvgl/ChatList.h @@ -111,8 +111,8 @@ static lv_obj_t* sectionTitle(lv_obj_t* parent, const char* text) { } // Messages section title: a chevron and the title fold the section (tap), -// an "All" / "★ Fav" pill on the right (filter >= 0) flips its -// favourites-only filter. Folded, the title carries the unread count. +// an All / Favourites choice on the right (filter >= 0), as the Nodes list's +// toolbar has. Folded, the title carries the unread count. enum : uint8_t { FOLD_CHANNELS, FOLD_DIRECT, FOLD_ROOMS }; static int s_chat_fold = -1; // bit per FOLD_*; -1: not read from NVS yet static void onChatFold(lv_event_t* e) { s_ui->chatFold((int)(uintptr_t)lv_event_get_user_data(e)); } @@ -121,6 +121,13 @@ static bool chatFolded(int which) { return s_chat_fold & (1 << which); } +// A section's favourites-only filter: user data = which | what it was << 8. +static void chatFilterChoice(lv_obj_t* row, int which, bool fav_only) { + lv_obj_t* c = toolbarChoice(row, "All\nFavourites", fav_only ? 1 : 0, "Show"); + lv_obj_align(c, LV_ALIGN_RIGHT_MID, 0, 0); + lv_obj_add_event_cb(c, onChatFilter, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)(which | (fav_only ? 0x100 : 0))); +} + static void chatSection(lv_obj_t* parent, const char* text, int fold, int unread, int filter, bool fav_only) { bool folded = chatFolded(fold); lv_obj_t* row = lv_obj_create(parent); @@ -140,18 +147,17 @@ static void chatSection(lv_obj_t* parent, const char* text, int fold, int unread lv_obj_align_to(n, t, LV_ALIGN_OUT_RIGHT_MID, 8, 0); } if (filter < 0 || folded) return; - styleSelected(pillButton(row, fav_only ? UI_SYMBOL_STAR " Fav" : "All", theme::SURFACE, onChatFilter, filter), fav_only); + chatFilterChoice(row, filter, fav_only); } -// Section title with an "All" / "★ Fav" pill on the right that flips the -// section's favourites-only filter. +// Section title with the All / Favourites choice on the right. static void sectionWithFilter(lv_obj_t* parent, const char* text, bool fav_only, uint8_t which) { lv_obj_t* row = lv_obj_create(parent); styleSurface(row, theme::BG); lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(row, LV_PCT(100), 26); - lv_obj_align(label(row, text, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_BOTTOM_LEFT, 0, -2); - styleSelected(pillButton(row, fav_only ? UI_SYMBOL_STAR " Fav" : "All", theme::SURFACE, onChatFilter, which), fav_only); + lv_obj_set_size(row, LV_PCT(100), 30); + lv_obj_align(label(row, text, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_LEFT_MID, 0, 0); + chatFilterChoice(row, which, fav_only); } void UITask::showChats() { @@ -165,14 +171,9 @@ static lv_obj_t* headerButton(lv_obj_t* hdr, const char* text, lv_event_cb_t cb, void UITask::buildChats() { lv_obj_t* body = newScreen("Messages", true); s_chats_read_all = unreadTotal() > 0; - if (_header) { // a new message first, whatever the lists below hold - lv_obj_t* nb = headerButton(_header, LV_SYMBOL_EDIT " New", onNewChat, 4, NULL); - stylePrimary(nb); - if (s_chats_read_all) { - lv_obj_t* rb = headerButton(_header, LV_SYMBOL_OK " Read all", onMarkAllRead, 0, NULL); - layoutNow(_header); - lv_obj_align_to(rb, nb, LV_ALIGN_OUT_LEFT_MID, -6, 0); - } + if (_header) { // a new message last, at the edge, whatever the lists below hold + if (s_chats_read_all) headerButton(_header, LV_SYMBOL_OK " Read all", onMarkAllRead, 0, NULL); + stylePrimary(headerButton(_header, LV_SYMBOL_EDIT, onNewChat, 0, NULL)); // the pencil alone: the title fits } vlist::attach(body); fillChats(); @@ -281,7 +282,7 @@ void UITask::fillChats() { vlist::add(body, &DM, rows); rows++; } - if (rows == 0 && !chatFolded(FOLD_DIRECT)) { label(body, "No conversations yet - New, top right", THEME_FONT_SMALL, theme::TEXT_MUTED); own(); } + if (rows == 0 && !chatFolded(FOLD_DIRECT)) { label(body, "No conversations yet - the pencil, top right", THEME_FONT_SMALL, theme::TEXT_MUTED); own(); } // Room servers: tap logs in (saved password or ask) and opens; hold: options static const vlist::Kind ROOM = { diff --git a/examples/companion_radio/ui-lvgl/ClockScreen.h b/examples/companion_radio/ui-lvgl/ClockScreen.h index c0d70217..86ed592d 100644 --- a/examples/companion_radio/ui-lvgl/ClockScreen.h +++ b/examples/companion_radio/ui-lvgl/ClockScreen.h @@ -236,7 +236,7 @@ static lv_obj_t* wheel(lv_obj_t* parent, uint8_t n, int sel, lv_event_cb_t cb, u *w = { n, (int32_t)(sel % n) * 256, 0, 0 }; lv_obj_t* o = lv_obj_create(parent); styleSurface(o, theme::SURFACE); - lv_obj_set_style_radius(o, theme::RADIUS_SM, 0); + lv_obj_set_style_radius(o, theme::RADIUS, 0); lv_obj_remove_flag(o, (lv_obj_flag_t)(LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_SCROLL_CHAIN)); lv_obj_set_size(o, 60, 84); // three rows; leaves room for the button below lv_obj_set_user_data(o, w); @@ -251,7 +251,7 @@ static lv_obj_t* colon(lv_obj_t* parent) { return label(parent, ":", THEME_FONT_ } // namespace clockview static void onOpenClock(lv_event_t* e) { (void)e; s_ui->showClock(); } -static void onClockTab(lv_event_t* e) { s_ui->clockTab((int)(uintptr_t)lv_event_get_user_data(e)); } +static void onClockTab(lv_event_t* e) { s_ui->clockTab((int)lv_buttonmatrix_get_selected_button((lv_obj_t*)lv_event_get_target(e))); } static void onClockAct(lv_event_t* e) { s_ui->clockAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); } static void onRingDismiss(lv_event_t* e) { (void)e; s_ui->dismissRing(); } @@ -293,18 +293,9 @@ void UITask::buildClock() { s_big = s_go_lbl = s_alarm_sw = s_cal = nullptr; // Tabs - lv_obj_t* tabs = centerRow(body); - static const char* const NAMES[TAB_COUNT] = { "Alarm", "Timer", "Stopwatch", "Calendar" }; - for (int t = 0; t < TAB_COUNT; t++) { - lv_obj_t* b = lv_button_create(tabs); - lv_obj_set_height(b, 32); - lv_obj_set_flex_grow(b, 1); - lv_obj_set_style_shadow_width(b, 0, 0); - lv_obj_set_style_radius(b, theme::RADIUS, 0); - lv_obj_add_event_cb(b, onClockTab, LV_EVENT_CLICKED, (void*)(uintptr_t)t); - lv_obj_center(label(b, NAMES[t], THEME_FONT_SMALL, theme::TEXT)); - styleSelected(b, t == s_tab); - } + static const char* TABS[TAB_COUNT + 1] = { "Alarm", "Timer", "Stopwatch", "Calendar", "" }; + lv_obj_t* tabs = segmented(body, TABS, s_tab, LV_PCT(100), theme::TAB_H, theme::SURFACE); + lv_obj_add_event_cb(tabs, onClockTab, LV_EVENT_VALUE_CHANGED, NULL); if (s_tab == TAB_ALARM) { lv_obj_t* r = centerRow(body); diff --git a/examples/companion_radio/ui-lvgl/ConversationScreen.h b/examples/companion_radio/ui-lvgl/ConversationScreen.h index b6d6d775..25b53f58 100644 --- a/examples/companion_radio/ui-lvgl/ConversationScreen.h +++ b/examples/companion_radio/ui-lvgl/ConversationScreen.h @@ -90,7 +90,10 @@ static void onMsgHold(lv_event_t* e) { lv_indev_wait_release(lv_indev_active()); s_ui->messageMenu(threadRowOf((lv_obj_t*)lv_event_get_current_target(e))); } -static void onChatFilter(lv_event_t* e) { s_ui->toggleChatFilter((uint8_t)(uintptr_t)lv_event_get_user_data(e)); } +static void onChatFilter(lv_event_t* e) { // (chatFilterChoice) the other one picked: flipped + const uintptr_t u = (uintptr_t)lv_event_get_user_data(e); + if ((choiceSelected((lv_obj_t*)lv_event_get_target(e)) == 1) != ((u & 0x100) != 0)) s_ui->toggleChatFilter((uint8_t)(u & 0xFF)); +} // ── Rooms ───────────────────────────────────────────────────────────────────── diff --git a/examples/companion_radio/ui-lvgl/DeviceScreen.h b/examples/companion_radio/ui-lvgl/DeviceScreen.h index 7fe5cfc7..3ca747da 100644 --- a/examples/companion_radio/ui-lvgl/DeviceScreen.h +++ b/examples/companion_radio/ui-lvgl/DeviceScreen.h @@ -147,6 +147,8 @@ void UITask::lockScreen() { lv_obj_remove_style_all(s_lock); lv_obj_set_size(s_lock, LV_PCT(100), lv_display_get_vertical_resolution(NULL) - theme::STATUS_H); lv_obj_set_pos(s_lock, 0, theme::STATUS_H); // the status bar stays visible + s_sb_lock = true; // up over a title bar too + statusBarSync(); lv_obj_set_style_bg_color(s_lock, lv_color_hex(theme::BG), 0); lv_obj_set_style_bg_opa(s_lock, LV_OPA_COVER, 0); lv_obj_add_flag(s_lock, LV_OBJ_FLAG_CLICKABLE); // swallows every touch but the slider's @@ -222,6 +224,8 @@ void UITask::unlockScreen() { } s_lock = s_lock_clock = s_lock_date = s_lock_unread = s_lock_slider = s_pin_dots = s_pin_msg = nullptr; _pin_entry[0] = '\0'; + s_sb_lock = false; + statusBarSync(); if (_screen == SCR_HOME) refreshHome(); else if (_screen == SCR_CHATS) refreshChats(); // counts moved on while locked else if (_screen == SCR_THREAD) reopenThreadIfUnread(); // on to what came in meanwhile @@ -432,7 +436,7 @@ void UITask::favGrid(lv_obj_t* grid) { bool used = contactctl::favName(_prefs, s, name, sizeof(name), &c); bool chan = favslots::kind(_prefs, s) == NodePrefs::FAV_KIND_CHANNEL; lv_obj_t* b = lv_button_create(grid); - lv_obj_set_size(b, w, 76); + lv_obj_set_size(b, w, homeTileH(2, lv_font_get_line_height(THEME_FONT_SMALL) + theme::GAP)); // the hint under them lv_obj_set_style_pad_all(b, 4, 0); lv_obj_set_style_radius(b, theme::RADIUS, 0); lv_obj_set_style_shadow_width(b, 0, 0); @@ -445,12 +449,12 @@ void UITask::favGrid(lv_obj_t* grid) { lv_obj_add_event_cb(b, onFavTap, LV_EVENT_CLICKED, (void*)(uintptr_t)s); lv_obj_add_event_cb(b, onFavHold, LV_EVENT_LONG_PRESSED, (void*)(uintptr_t)s); const char* icon = !used ? LV_SYMBOL_PLUS : chan ? "#" : c.type == ADV_TYPE_ROOM ? LV_SYMBOL_HOME : UI_SYMBOL_USERS; - lv_obj_align(label(b, icon, THEME_FONT_LARGE, used ? theme::ACCENT : theme::TEXT_MUTED), LV_ALIGN_TOP_MID, 0, 10); + lv_obj_align(label(b, icon, THEME_FONT_LARGE, used ? theme::ACCENT : theme::TEXT_MUTED), LV_ALIGN_CENTER, 0, -11); lv_obj_t* n = label(b, used ? name : "Add", THEME_FONT_SMALL, used ? theme::TEXT : theme::TEXT_MUTED); lv_label_set_long_mode(n, LV_LABEL_LONG_DOT); 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); + lv_obj_align(n, LV_ALIGN_CENTER, 0, 14); 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); diff --git a/examples/companion_radio/ui-lvgl/DiagScreen.h b/examples/companion_radio/ui-lvgl/DiagScreen.h index 772e06bd..a82478fb 100644 --- a/examples/companion_radio/ui-lvgl/DiagScreen.h +++ b/examples/companion_radio/ui-lvgl/DiagScreen.h @@ -336,7 +336,7 @@ void UITask::buildDiag() { if (s_tab == TAB_LIVE && _header) headerButton(_header, LV_SYMBOL_REFRESH " Reset", onDiagReset, 4, NULL); static const char* TABS[] = { "Live", "History", "System", "Font", "Noise", "" }; - lv_obj_t* tabs = segmented(body, TABS, s_tab, lv_pct(100), 34); + lv_obj_t* tabs = segmented(body, TABS, s_tab, lv_pct(100), theme::TAB_H); lv_obj_set_style_bg_color(tabs, lv_color_hex(theme::SURFACE), LV_PART_ITEMS); lv_obj_add_event_cb(tabs, onDiagTab, LV_EVENT_VALUE_CHANGED, NULL); diff --git a/examples/companion_radio/ui-lvgl/GpsScreen.h b/examples/companion_radio/ui-lvgl/GpsScreen.h index 2256137f..6698e4ff 100644 --- a/examples/companion_radio/ui-lvgl/GpsScreen.h +++ b/examples/companion_radio/ui-lvgl/GpsScreen.h @@ -257,10 +257,9 @@ void UITask::buildGps() { s_cn0 = s_best = s_nmea = s_rx = s_d_fix = s_d_ttff = s_d_search = s_d_utc = nullptr; for (lv_obj_t*& p : s_sys_pill) p = nullptr; lv_obj_set_style_pad_row(body, 6, 0); - if (_header) { + { // the tabs first under the title bar, as on every screen with tabs static const char* TABS[] = { "Sky", "Signal", "Details", "" }; - lv_obj_t* tabs = segmented(_header, TABS, s_tab, 186, 28, theme::SURFACE); - lv_obj_align(tabs, LV_ALIGN_RIGHT_MID, -4, 0); + lv_obj_t* tabs = segmented(body, TABS, s_tab, LV_PCT(100), theme::TAB_H, theme::SURFACE); lv_obj_add_event_cb(tabs, onGpsTab, LV_EVENT_VALUE_CHANGED, NULL); } diff --git a/examples/companion_radio/ui-lvgl/HomeScreen.h b/examples/companion_radio/ui-lvgl/HomeScreen.h index 0af542e2..152a6046 100644 --- a/examples/companion_radio/ui-lvgl/HomeScreen.h +++ b/examples/companion_radio/ui-lvgl/HomeScreen.h @@ -179,19 +179,19 @@ static lv_obj_t* dot(uint8_t kind) { static lv_obj_t* homeTile(lv_obj_t* parent, const char* icon, const char* text, lv_event_cb_t cb, int badge_n) { int w = (lv_display_get_horizontal_resolution(NULL) - 2 * theme::PAD - 2 * theme::GAP) / 3; lv_obj_t* b = lv_button_create(parent); - lv_obj_set_size(b, w, 76); + lv_obj_set_size(b, w, homeTileH(2)); // two rows fill the page lv_obj_set_style_pad_all(b, 4, 0); lv_obj_set_style_radius(b, theme::RADIUS, 0); lv_obj_set_style_shadow_width(b, 0, 0); lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE), 0); lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); if (cb) lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, NULL); - lv_obj_align(label(b, icon, THEME_FONT_LARGE, theme::ACCENT), LV_ALIGN_TOP_MID, 0, 10); + lv_obj_align(label(b, icon, THEME_FONT_LARGE, theme::ACCENT), LV_ALIGN_CENTER, 0, -11); // icon over name, centred lv_obj_t* n = label(b, text, THEME_FONT_SMALL, theme::TEXT); lv_label_set_long_mode(n, LV_LABEL_LONG_DOT); 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); + lv_obj_align(n, LV_ALIGN_CENTER, 0, 14); tileBadge(b, badge_n); return b; } @@ -441,7 +441,7 @@ void UITask::goHome() { void UITask::buildHome() { lv_obj_t* body = newScreen(NULL, false); lv_obj_remove_flag(body, LV_OBJ_FLAG_SCROLLABLE); // a drag is a page swipe, not a scroll - lv_obj_set_style_pad_bottom(body, 26, 0); // the dots + lv_obj_set_style_pad_bottom(body, HOME_DOTS_H, 0); // the dots home::s_page_box = lv_obj_create(body); styleSurface(home::s_page_box, theme::BG); lv_obj_remove_flag(home::s_page_box, LV_OBJ_FLAG_SCROLLABLE); @@ -473,7 +473,7 @@ void UITask::buildHome() { lv_obj_add_flag(b, LV_OBJ_FLAG_IGNORE_LAYOUT); lv_obj_set_size(b, LV_SIZE_CONTENT, 26); lv_obj_set_style_pad_hor(b, 14, 0); - lv_obj_set_style_radius(b, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_radius(b, theme::RADIUS, 0); lv_obj_set_style_shadow_width(b, 0, 0); lv_obj_set_style_bg_color(b, lv_color_hex(theme::ACCENT), 0); lv_obj_align(b, LV_ALIGN_BOTTOM_RIGHT, 0, 24); diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index c7f73a30..c1b92a19 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -493,7 +493,11 @@ static int hitMark(int px, int py) { } // namespace mapview namespace navmap { // shared with NavMap.h (included after this file) -static const int BAR_H = 44; // target bar along the bottom of the Navigation map +static const int BAR_H = 44; // target bar along the bottom of the Navigation map, while there is a target +static lv_obj_t* s_scale_pill = nullptr; // the bottom-left pill (credit, scale, zoom): moves up over a bar +static bool s_ready = false; // the map is built: a bar coming or going lays it out again +static lv_obj_t* s_tools_list = nullptr; // Map tools' list while it's up +static int32_t s_tools_y = 0; // where it was scrolled, leaving for a page of options // Navigation targets as one int (event user data): type << 8 | index. enum : uint8_t { T_CLEAR, T_WAYPOINT, T_TRAILSTART, T_LIVE }; static int code(uint8_t type, int idx) { return (type << 8) | (idx & 0xFF); } @@ -577,7 +581,21 @@ static lv_obj_t* mapPill(lv_obj_t* parent, const char* text) { // Centring on a point puts it mid-way down the visible map: above the nav bar // on the Navigation map (the bias is in tiles, added to the view centre). double UITask::mapCenterBias() const { - return _map_nav ? navmap::BAR_H / 2.0 / mapview::TILE_PX : 0.0; + return mapBottomH() / 2.0 / mapview::TILE_PX; +} + +// The bar along the map's bottom edge: the target's (Navigation map, while +// there is one) or the route pick's. +int UITask::mapBottomH() const { + if (_route_pick) return navmap::BAR_H; + return _map_nav && _nav_bar && !lv_obj_has_flag(_nav_bar, LV_OBJ_FLAG_HIDDEN) ? navmap::BAR_H : 0; +} + +// The controls along the bottom edge, clear of that bar. +void UITask::mapPlaceBottom() { + const int bottom = mapBottomH(); + if (_map_center_btn) lv_obj_align(_map_center_btn, LV_ALIGN_BOTTOM_RIGHT, -6, -6 - bottom); + if (navmap::s_scale_pill) lv_obj_align(navmap::s_scale_pill, LV_ALIGN_BOTTOM_LEFT, 6, -6 - bottom); } #ifdef UI_PERF_TEST @@ -615,7 +633,7 @@ void UITask::openMap(bool nav) { showMap(); } -void UITask::showMap() { +void UITask::showMap(bool frame) { _screen = SCR_MAP; mapview::s_trails_on = lvport::trailsOn(); mapview::s_provider = lvport::vectorOn() ? (mapview::TileProvider*)&mapview::s_vector : &mapview::s_raster; @@ -634,7 +652,7 @@ void UITask::showMap() { _map_cy = mapview::latToTileY(la, z); } buildMap(); - if (_map_nav && _prefs && _prefs->locator_has_target) navFrameTarget(); // show where we're going + if (frame && _map_nav && _prefs && _prefs->locator_has_target) navFrameTarget(); // show where we're going } void UITask::buildMap() { @@ -713,9 +731,8 @@ void UITask::buildMap() { // left column (back, pin, tools) -- the right one has no room above the bar. if (_map_nav) lv_obj_align(mapButton(body, LV_SYMBOL_BARS, onNavTools), LV_ALIGN_TOP_LEFT, 6, 98); else lv_obj_align(mapButton(body, LV_SYMBOL_DOWNLOAD, onMapDownload), LV_ALIGN_TOP_RIGHT, -6, 98); - int bottom = _map_nav ? navmap::BAR_H : 0; // the nav bar takes the bottom edge - _map_center_btn = mapButton(body, LV_SYMBOL_GPS, onMapCenter); - lv_obj_align(_map_center_btn, LV_ALIGN_BOTTOM_RIGHT, -6, -6 - bottom); + navmap::s_ready = false; + _map_center_btn = mapButton(body, LV_SYMBOL_GPS, onMapCenter); // bottom right (mapPlaceBottom) // Top centre: what's going on, one pill under another -- the trail / // live share (NavMap.h), the download. Empty most of the time. mapview::s_going = lv_obj_create(body); @@ -761,7 +778,7 @@ void UITask::buildMap() { lv_obj_set_style_border_side(mapview::s_scale_bar, (lv_border_side_t)(LV_BORDER_SIDE_BOTTOM | LV_BORDER_SIDE_LEFT | LV_BORDER_SIDE_RIGHT), 0); mapview::s_scale_lbl = label(sc, "", THEME_FONT_SMALL, theme::TEXT); _map_zoom_lbl = label(sc, "", THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_obj_align(sc, LV_ALIGN_BOTTOM_LEFT, 6, -6 - bottom); + navmap::s_scale_pill = sc; // bottom left (mapPlaceBottom) _map_hint = mapPill(body, ""); lv_label_set_long_mode(_map_hint, LV_LABEL_LONG_WRAP); lv_obj_set_width(_map_hint, 200); @@ -772,11 +789,13 @@ void UITask::buildMap() { if (_map_nav) buildNavControls(body); _route_bar = _route_lbl = nullptr; if (_route_pick) buildRouteBar(body); + mapPlaceBottom(); layoutNow(body); mapview::s_mark_count = 0; // the previous markers went with the previous screen rebuildMapMarkers(); layoutMap(); + navmap::s_ready = true; } // Places tiles and markers for the current centre / zoom; queues tiles that @@ -847,7 +866,7 @@ void UITask::layoutMap() { mapview::showTiled(_map_area, mapview::s_grid, tiled); mapview::labels::layout(left, top, w, h, _map_z, have_provider && mapview::s_provider == &mapview::s_vector && mapview::s_vector.hasData(), - _map_nav ? navmap::BAR_H : 0); + mapBottomH()); // Own position int32_t lat, lon; @@ -1019,7 +1038,7 @@ void UITask::layoutMapMarks(double left, double top, int w, int h) { struct Box { int16_t x1, y1, x2, y2; }; static Box used[6 + MAX_MARKS + MARK_LABELS_MAX]; // the map's controls, the dots, the names int nu = 0; - const int bottom = _map_nav || _route_pick ? navmap::BAR_H : 0; // the nav bar, or the route's + const int bottom = mapBottomH(); // the target's bar, or the route's const int16_t W = (int16_t)w, H = (int16_t)(h - bottom); used[nu++] = { 0, 0, 50, 144 }; // button columns used[nu++] = { (int16_t)(W - 50), 0, W, 144 }; diff --git a/examples/companion_radio/ui-lvgl/NavMap.h b/examples/companion_radio/ui-lvgl/NavMap.h index c53c2bce..26e88348 100644 --- a/examples/companion_radio/ui-lvgl/NavMap.h +++ b/examples/companion_radio/ui-lvgl/NavMap.h @@ -166,6 +166,7 @@ void UITask::buildNavControls(lv_obj_t* body) { lv_obj_set_style_pad_hor(_nav_bar, theme::PAD, 0); lv_obj_set_style_pad_ver(_nav_bar, 3, 0); lv_obj_add_event_cb(_nav_bar, onNavList, LV_EVENT_CLICKED, NULL); + lv_obj_add_flag(_nav_bar, LV_OBJ_FLAG_HIDDEN); // until there is a target (refreshNavBar) _nav_title = label(_nav_bar, "", THEME_FONT_BODY, theme::ACCENT); lv_label_set_long_mode(_nav_title, LV_LABEL_LONG_DOT); lv_obj_set_size(_nav_title, 260, 19); // fixed height: LONG_DOT cuts instead of wrapping @@ -393,15 +394,14 @@ void UITask::refreshNavBar() { char name[40]; bool person = false, set = false; bool pos = navCurrentTarget(tlat, tlon, name, sizeof(name), person, set); - if (!set) { - setText(_nav_title, "No target"); - setTextColor(_nav_title, theme::TEXT_MUTED); - setText(_nav_info, "Tap here to pick a target, or hold the map on a spot"); - lv_obj_add_flag(_nav_clear, LV_OBJ_FLAG_HIDDEN); - return; + // The bar only while there is a target: the map gets the whole screen + // otherwise (a target comes from the pin's list or a hold on the map). + if (set == lv_obj_has_flag(_nav_bar, LV_OBJ_FLAG_HIDDEN)) { + lv_obj_set_flag(_nav_bar, LV_OBJ_FLAG_HIDDEN, !set); + mapPlaceBottom(); + if (navmap::s_ready) layoutMap(); // the names and markers clear of it, or not } - setTextColor(_nav_title, theme::ACCENT); - lv_obj_remove_flag(_nav_clear, LV_OBJ_FLAG_HIDDEN); + if (!set) return; setTextFmt(_nav_title, "%s %s", navmap::s_tb.active() ? LV_SYMBOL_LOOP : person ? UI_SYMBOL_USERS : UI_SYMBOL_FLAG, name); if (!pos) { @@ -501,14 +501,14 @@ void UITask::navFrameTrail() { } _map_follow = false; int w = _map_area ? lv_obj_get_width(_map_area) : 320; - int h = (_map_area ? lv_obj_get_height(_map_area) : 218) - navmap::BAR_H; + int h = (_map_area ? lv_obj_get_height(_map_area) : 218) - mapBottomH(); double dx = (x1 - x0) * mapview::TILE_PX, dy = (y1 - y0) * mapview::TILE_PX; int z = mapview::MAX_Z - 1; while (z > mapview::MIN_Z && (dx * (1 << z) > w - 110 || dy * (1 << z) > h - 60)) z--; // clear of the buttons _map_z = z; double n = (double)(1 << z); _map_cx = (x0 + x1) / 2 * n; - _map_cy = (y0 + y1) / 2 * n + navmap::BAR_H / 2.0 / mapview::TILE_PX; + _map_cy = (y0 + y1) / 2 * n + mapCenterBias(); layoutMap(); } @@ -591,6 +591,7 @@ void UITask::navClosePopup() { if (_nav_overlay) lv_obj_delete_async(_nav_overlay); // may be closing from its own button _nav_overlay = _nav_ta = _nav_kb = _nav_del_lbl = nullptr; _nav_trail_lbl = _nav_trail_btn = _nav_reset_lbl = _nav_share_lbl = _nav_share_btn = _nav_tb_btn = nullptr; + navmap::s_tools_list = nullptr; } lv_obj_t* UITask::navTextEntry(const TextEntry& t) { @@ -626,76 +627,6 @@ static void navRowRight(lv_obj_t* row, const char* text, uint32_t col, int right lv_obj_align(label(row, text, THEME_FONT_SMALL, col), LV_ALIGN_RIGHT_MID, -right, 0); } -void UITask::navTargetsPopup() { - lv_obj_t* panel = navPopupPanel("Navigate to", true); - lv_obj_t* list = scrollList(panel); - - int32_t lat = 0, lon = 0; - bool gps = _core->course.currentLocation(lat, lon); - bool imperial = _prefs && _prefs->units_imperial; - auto dist = [&](int32_t la, int32_t lo, char* out, size_t n) { - if (gps) geo::fmtDist(out, n, geo::haversineKm(lat, lon, la, lo), imperial); - else out[0] = '\0'; - }; - - if (_prefs && _prefs->locator_has_target) { - char sub[40]; - snprintf(sub, sizeof(sub), "Now: %s", _prefs->locator_label[0] ? _prefs->locator_label : "target"); - listRow(list, LV_SYMBOL_CLOSE " Clear target", sub, onNavPick, (void*)(uintptr_t)navmap::code(navmap::T_CLEAR, 0)); - } - - const WaypointModel& wp = _core->waypoints; - char sec[32]; - snprintf(sec, sizeof(sec), "WAYPOINTS %d/%d", wp.count(), WaypointStore::CAPACITY); - sectionTitle(list, sec); - for (int i = 0; i < wp.count(); i++) { - const Waypoint& w = wp.at(i); - char title[WAYPOINT_LABEL_LEN + 8], sub[40], d[12]; - snprintf(title, sizeof(title), UI_SYMBOL_FLAG " %s", w.label[0] ? w.label : "(unnamed)"); - snprintf(sub, sizeof(sub), "%.5f, %.5f", w.lat_1e6 / 1e6, w.lon_1e6 / 1e6); - lv_obj_t* row = listRow(list, title, sub, onNavPick, (void*)(uintptr_t)navmap::code(navmap::T_WAYPOINT, i)); - lv_obj_t* more = lv_button_create(row); // the waypoint menu - lv_obj_set_size(more, 40, 34); - lv_obj_align(more, LV_ALIGN_RIGHT_MID, -4, 0); - lv_obj_set_style_shadow_width(more, 0, 0); - lv_obj_set_style_radius(more, theme::RADIUS, 0); - lv_obj_set_style_bg_color(more, lv_color_hex(theme::SURFACE_2), 0); - lv_obj_add_event_cb(more, onNavWpMenu, LV_EVENT_CLICKED, (void*)(uintptr_t)i); - lv_obj_center(label(more, LV_SYMBOL_EDIT, THEME_FONT_BODY, theme::TEXT)); - dist(w.lat_1e6, w.lon_1e6, d, sizeof(d)); - if (d[0]) navRowRight(row, d, theme::TEXT_MUTED, 52); - } - if (wp.count() == 0) { - noteLabel(list, "None yet. Hold the map to drop one, or tap the pin to mark where you are."); - } - TrailStore& ts = _core->trail.store(); - if (!ts.empty()) { - char d[12], sub[24]; - dist(ts.first().lat_1e6, ts.first().lon_1e6, d, sizeof(d)); - snprintf(sub, sizeof(sub), "%d trail points", ts.count()); - lv_obj_t* row = listRow(list, LV_SYMBOL_HOME " Trail start", sub, onNavPick, - (void*)(uintptr_t)navmap::code(navmap::T_TRAILSTART, 0)); - if (d[0]) navRowRight(row, d, theme::TEXT_MUTED, theme::PAD); - } - - const LiveTrackStore& lt = _core->live_share.track(); - uint32_t now = rtc_clock.getCurrentTime(); - sectionTitle(list, "SHARING LIVE"); - int live = 0; - for (int i = 0; i < LiveTrackStore::CAPACITY; i++) { - if (!lt.isActive(i, now)) continue; - const LiveTrackStore::Entry& e = lt.slotAt(i); - char age[8], sub[40], d[12]; - geo::fmtAgeShort(age, sizeof(age), now, e.ts); - snprintf(sub, sizeof(sub), "%s - %s ago", e.verified ? "direct" : "channel", age[0] ? age : "0s"); - lv_obj_t* row = listRow(list, e.name, sub, onNavPick, (void*)(uintptr_t)navmap::code(navmap::T_LIVE, i)); - dist(e.lat_1e6, e.lon_1e6, d, sizeof(d)); - if (d[0]) navRowRight(row, d, theme::OK, theme::PAD); - live++; - } - if (live == 0) label(list, "Nobody is sharing their position.", THEME_FONT_SMALL, theme::TEXT_MUTED); -} - // A place's coordinates and, with a fix, how far and which way. void UITask::placeCard(lv_obj_t* parent, int32_t lat, int32_t lon) { lv_obj_t* card = infoCard(parent); @@ -968,9 +899,11 @@ static lv_obj_t* toolButton(lv_obj_t* bar, const char* text, uint8_t act, bool a return lv_obj_get_child(barButton(bar, text, onNavTool, act, accent), 0); } -void UITask::navToolsPopup() { +void UITask::navToolsPopup(bool back) { + if (back) s_pop_still = true; // it never went away, as far as the eye goes lv_obj_t* panel = navPopupPanel("Map tools", true); lv_obj_t* list = scrollList(panel); + navmap::s_tools_list = list; sectionTitle(list, "TRAIL"); _nav_trail_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT); @@ -1057,6 +990,10 @@ void UITask::navToolsPopup() { #endif refreshNavTools(); + if (back) { // scrolled where it was left + layoutNow(panel); + lv_obj_scroll_to_y(list, navmap::s_tools_y, LV_ANIM_OFF); + } } void UITask::refreshNavTools() { @@ -1409,6 +1346,7 @@ void UITask::navToolAction(uint8_t act) { case navmap::TL_OPT_TRAIL: // one section of the Map options; back returns to the map case navmap::TL_OPT_SHARE: case navmap::TL_OPT_ALERT: + if (navmap::s_tools_list) navmap::s_tools_y = lv_obj_get_scroll_y(navmap::s_tools_list); // back to here showMapOptions(act == navmap::TL_OPT_TRAIL ? settings::SEC_TRAIL : act == navmap::TL_OPT_SHARE ? settings::SEC_LIVE_SHARE : settings::SEC_LOCATOR); return; @@ -1440,35 +1378,84 @@ static void onNavCoordsKb(lv_event_t* e) { s_ui->navCoordsDone(lv_event_get_code // The pin button: every waypoint (tap for its menu), plus the two ways to add // one that aren't a long-press -- here (GPS, averaged per Settings) or typed. -void UITask::navWaypointsPopup() { +// The places the map knows -- waypoints, the trail start, people sharing +// live -- to navigate to (a tap) or keep (a waypoint added here, by its +// coordinates, or by holding the map). From the pin button and the target bar. +void UITask::navTargetsPopup() { const WaypointModel& wp = _core->waypoints; - char title[32]; - snprintf(title, sizeof(title), "Waypoints %d/%d", wp.count(), WaypointStore::CAPACITY); - lv_obj_t* panel = navPopupPanel(title, true); + lv_obj_t* panel = navPopupPanel("Places", true); lv_obj_t* r = buttonBar(panel); - toolButton(r, UI_SYMBOL_PIN " Here (GPS)", navmap::TL_WP_HERE, true); + toolButton(r, UI_SYMBOL_PIN " Mark here", navmap::TL_WP_HERE, true); toolButton(r, LV_SYMBOL_KEYBOARD " Coordinates", navmap::TL_WP_COORDS, false); - lv_obj_t* list = scrollList(panel); + int32_t lat = 0, lon = 0; bool gps = _core->course.currentLocation(lat, lon); + bool imperial = _prefs && _prefs->units_imperial; + auto dist = [&](int32_t la, int32_t lo, char* out, size_t n) { + if (gps) geo::fmtDist(out, n, geo::haversineKm(lat, lon, la, lo), imperial); + else out[0] = '\0'; + }; + + if (_prefs && _prefs->locator_has_target) { + char sub[40]; + snprintf(sub, sizeof(sub), "Now: %s", _prefs->locator_label[0] ? _prefs->locator_label : "target"); + listRow(list, LV_SYMBOL_CLOSE " Clear target", sub, onNavPick, (void*)(uintptr_t)navmap::code(navmap::T_CLEAR, 0)); + } + + char sec[32]; + snprintf(sec, sizeof(sec), "WAYPOINTS %d/%d", wp.count(), WaypointStore::CAPACITY); + sectionTitle(list, sec); for (int i = 0; i < wp.count(); i++) { const Waypoint& w = wp.at(i); - char t[WAYPOINT_LABEL_LEN + 8], sub[40]; - snprintf(t, sizeof(t), UI_SYMBOL_FLAG " %s", w.label[0] ? w.label : "(unnamed)"); + char title[WAYPOINT_LABEL_LEN + 8], sub[40], d[12]; + snprintf(title, sizeof(title), UI_SYMBOL_FLAG " %s", w.label[0] ? w.label : "(unnamed)"); snprintf(sub, sizeof(sub), "%.5f, %.5f", w.lat_1e6 / 1e6, w.lon_1e6 / 1e6); - lv_obj_t* row = listRow(list, t, sub, onNavWpMenu, (void*)(uintptr_t)i); - if (gps) { - char d[12]; - geo::fmtDist(d, sizeof(d), geo::haversineKm(lat, lon, w.lat_1e6, w.lon_1e6), _prefs && _prefs->units_imperial); - navRowRight(row, d, theme::TEXT_MUTED, theme::PAD); - } + lv_obj_t* row = listRow(list, title, sub, onNavPick, (void*)(uintptr_t)navmap::code(navmap::T_WAYPOINT, i)); + lv_obj_t* more = lv_button_create(row); // the waypoint menu + lv_obj_set_size(more, 40, 34); + lv_obj_align(more, LV_ALIGN_RIGHT_MID, -4, 0); + lv_obj_set_style_shadow_width(more, 0, 0); + lv_obj_set_style_radius(more, theme::RADIUS, 0); + lv_obj_set_style_bg_color(more, lv_color_hex(theme::SURFACE_2), 0); + lv_obj_add_event_cb(more, onNavWpMenu, LV_EVENT_CLICKED, (void*)(uintptr_t)i); + lv_obj_center(label(more, LV_SYMBOL_EDIT, THEME_FONT_BODY, theme::TEXT)); + dist(w.lat_1e6, w.lon_1e6, d, sizeof(d)); + if (d[0]) navRowRight(row, d, theme::TEXT_MUTED, 52); } if (wp.count() == 0) { - noteLabel(list, "None yet. Add one here, type its coordinates, or hold the map on a spot."); + noteLabel(list, "None yet. Mark where you are, type coordinates, or hold the map on a spot."); } + TrailStore& ts = _core->trail.store(); + if (!ts.empty()) { + char d[12], sub[24]; + dist(ts.first().lat_1e6, ts.first().lon_1e6, d, sizeof(d)); + snprintf(sub, sizeof(sub), "%d trail points", ts.count()); + lv_obj_t* row = listRow(list, LV_SYMBOL_HOME " Trail start", sub, onNavPick, + (void*)(uintptr_t)navmap::code(navmap::T_TRAILSTART, 0)); + if (d[0]) navRowRight(row, d, theme::TEXT_MUTED, theme::PAD); + } + + const LiveTrackStore& lt = _core->live_share.track(); + uint32_t now = rtc_clock.getCurrentTime(); + sectionTitle(list, "SHARING LIVE"); + int live = 0; + for (int i = 0; i < LiveTrackStore::CAPACITY; i++) { + if (!lt.isActive(i, now)) continue; + const LiveTrackStore::Entry& e = lt.slotAt(i); + char age[8], sub[40], d[12]; + geo::fmtAgeShort(age, sizeof(age), now, e.ts); + snprintf(sub, sizeof(sub), "%s - %s ago", e.verified ? "direct" : "channel", age[0] ? age : "0s"); + lv_obj_t* row = listRow(list, e.name, sub, onNavPick, (void*)(uintptr_t)navmap::code(navmap::T_LIVE, i)); + dist(e.lat_1e6, e.lon_1e6, d, sizeof(d)); + if (d[0]) navRowRight(row, d, theme::OK, theme::PAD); + live++; + } + if (live == 0) label(list, "Nobody is sharing their position.", THEME_FONT_SMALL, theme::TEXT_MUTED); } +void UITask::navWaypointsPopup() { navTargetsPopup(); } // one list of places (above) + // "50.06142, 19.93721 Name" (decimal degrees; comma or space between them; the // name is optional) -> a new waypoint. void UITask::navCoordsPopup() { diff --git a/examples/companion_radio/ui-lvgl/NodesView.h b/examples/companion_radio/ui-lvgl/NodesView.h index 5dcf5f5d..3a3f5ebc 100644 --- a/examples/companion_radio/ui-lvgl/NodesView.h +++ b/examples/companion_radio/ui-lvgl/NodesView.h @@ -14,17 +14,22 @@ static lv_obj_t *s_nd_type, *s_nd_status, *s_nd_dist, *s_nd_pos, *s_nd_heard, *s enum : uint8_t { NODE_MSG, NODE_PING, NODE_FAV, NODE_ADD, NODE_DELETE, NODE_NAV, NODE_ADMIN, NODE_WAYPOINT, NODE_PIN, NODE_RESET_PATH, NODE_SHARE, NODE_ROUTE }; -static void onNearbyChip(lv_event_t* e) { s_ui->setNearbyFilter((uint8_t)(uintptr_t)lv_event_get_user_data(e)); } -static void onNearbySort(lv_event_t* e) { (void)e; s_ui->toggleNearbySort(); } +static void onNearbyChip(lv_event_t* e) { s_ui->setNearbyFilter((uint8_t)choiceSelected((lv_obj_t*)lv_event_get_target(e))); } +static void onNearbySort(lv_event_t* e) { s_ui->setNearbySort(choiceSelected((lv_obj_t*)lv_event_get_target(e)) == 1); } static void onNearbyScan(lv_event_t* e) { (void)e; s_ui->startNearbyScan(); } static void onNearbyRow(lv_event_t* e) { s_ui->openNode((int)vlist::arg(e)); } static void onScanRow(lv_event_t* e) { s_ui->openScanNode((int)vlist::arg(e)); } static void onScanClose(lv_event_t* e) { (void)e; s_ui->closeScanPopup(); } static void onNodeAction(lv_event_t* e) { s_ui->nodeAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); } +// A button in a title bar -- an icon, or an icon and a word -- in the row +// after the title, in the order made (`right` is ignored there); in a popup's +// own header, `right` px in from its right edge. static lv_obj_t* headerButton(lv_obj_t* hdr, const char* text, lv_event_cb_t cb, int right, lv_obj_t** label_out) { + const bool bar = lv_obj_get_style_height(hdr, LV_PART_MAIN) == theme::APPBAR_H; lv_obj_t* b = lv_button_create(hdr); - lv_obj_set_size(b, LV_SIZE_CONTENT, 28); + lv_obj_set_size(b, LV_SIZE_CONTENT, bar ? theme::BAR_BTN_H : 28); + lv_obj_set_style_min_width(b, 40, 0); // an icon alone: still a finger's width lv_obj_set_style_pad_hor(b, 10, 0); lv_obj_set_style_pad_ver(b, 0, 0); lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE), 0); @@ -47,33 +52,18 @@ void UITask::showNearby() { void UITask::buildNearby() { lv_obj_t* body = newScreen("Nodes", true); lv_obj_set_style_pad_row(body, 4, 0); - if (_header) { - headerButton(_header, LV_SYMBOL_REFRESH, onNearbyScan, 4, NULL); // scan - lv_obj_set_width(headerButton(_header, "", onNearbySort, 46, &_nearby_sort_lbl), 62); // fits "Recent" / "Dist" - headerButton(_header, UI_SYMBOL_MAP, onOpenNodesMap, 114, NULL); // the Nodes map (the advert: the quick panel) + if (_header) { // where they are, then who answers now (the advert: the quick panel) + headerButton(_header, UI_SYMBOL_MAP, onOpenNodesMap, 0, NULL); + headerButton(_header, LV_SYMBOL_REFRESH " Scan", onNearbyScan, 0, NULL); } - // Type filter chips - _nearby_chips = lv_obj_create(body); - styleSurface(_nearby_chips, theme::BG); - lv_obj_remove_flag(_nearby_chips, LV_OBJ_FLAG_SCROLLABLE); - lv_obj_set_size(_nearby_chips, LV_PCT(100), 26); - lv_obj_set_flex_flow(_nearby_chips, LV_FLEX_FLOW_ROW); - lv_obj_set_style_pad_column(_nearby_chips, 4, 0); - for (uint8_t f = 0; f < NearbyModel::F_COUNT; f++) { - lv_obj_t* c = lv_button_create(_nearby_chips); - lv_obj_set_height(c, 26); - lv_obj_set_flex_grow(c, 1); - lv_obj_set_style_pad_all(c, 0, 0); - lv_obj_set_style_radius(c, LV_RADIUS_CIRCLE, 0); - lv_obj_set_style_shadow_width(c, 0, 0); - bool on = f == _nearby->filter(); - lv_obj_add_event_cb(c, onNearbyChip, LV_EVENT_CLICKED, (void*)(uintptr_t)f); - lv_obj_center(label(c, NearbyModel::filterLabel(f), THEME_FONT_SMALL, theme::TEXT)); - styleSelected(c, on); // (Theme.h) - } - - _nearby_status = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED); + // One line over the list: how many, then which and in what order. + _nearby_status = toolbarRow(body); + lv_obj_t* bar = lv_obj_get_parent(_nearby_status); + lv_obj_add_event_cb(toolbarChoice(bar, "All\nFavourites\nCompanions\nRepeaters\nRooms\nSensors", _nearby->filter(), "Show"), + onNearbyChip, LV_EVENT_VALUE_CHANGED, NULL); + lv_obj_add_event_cb(toolbarChoice(bar, "Nearest\nLast heard", _nearby->sortMode() == NearbyModel::SORT_TIME ? 1 : 0, "Order"), + onNearbySort, LV_EVENT_VALUE_CHANGED, NULL); _nearby_list = scrollList(body); vlist::attach(_nearby_list); @@ -87,9 +77,8 @@ void UITask::setNearbyFilter(uint8_t f) { buildNearby(); } -void UITask::toggleNearbySort() { - _nearby->setSortMode(_nearby->sortMode() == NearbyModel::SORT_DIST ? NearbyModel::SORT_TIME - : NearbyModel::SORT_DIST); +void UITask::setNearbySort(bool last_heard) { + _nearby->setSortMode(last_heard ? NearbyModel::SORT_TIME : NearbyModel::SORT_DIST); _nearby_sig = 0; refreshNearbyList(); } @@ -244,12 +233,9 @@ void UITask::refreshNearbyList() { uint32_t sig = nearbySignature(); int n = _nearby->count(); - if (_nearby_sort_lbl) - setText(_nearby_sort_lbl, _nearby->sortMode() == NearbyModel::SORT_TIME ? "Recent" : "Dist"); int32_t lat, lon; bool gps = _nearby->ownPosition(lat, lon); - setTextFmt(_nearby_status, "%d node%s%s", n, n == 1 ? "" : "s", - gps ? "" : " - no GPS fix, distances unknown"); + setTextFmt(_nearby_status, "%d node%s%s", n, n == 1 ? "" : "s", gps ? "" : ", no GPS"); if (sig == _nearby_sig || vlist::pressed(_nearby_list)) return; // held: once it's let go _nearby_sig = sig; @@ -324,9 +310,25 @@ void UITask::buildNode() { const NearbyModel::Entry& e = s_node; lv_obj_t* body = newScreen(e.name[0] ? e.name : "(unknown)", true); - lv_obj_t* info = scrollList(body); // the info, scrolling above the actions at the bottom + lv_obj_t* info = scrollList(body); // the actions, then the info: the whole screen scrolls s_nd_fav = nullptr; _node_path_lbl = nullptr; + + // The bar: what the node is for -- a message, its admin, adding it -- a + // ping and the way there. The rest, less often, in a group under the info. + lv_obj_t* acts = buttonBar(info); + bool contact = e.contact_idx >= 0; + bool admin = contact && (e.type == ADV_TYPE_REPEATER || e.type == ADV_TYPE_ROOM); + bool pos = e.lat_e6 != 0 || e.lon_e6 != 0; + if (contact && e.type == ADV_TYPE_CHAT) barButton(acts, LV_SYMBOL_ENVELOPE " Message", onNodeAction, NODE_MSG, true); + if (admin) barButton(acts, LV_SYMBOL_SETTINGS " Admin", onNodeAction, NODE_ADMIN, true); + if (!contact && e.has_key && !e.is_known) barButton(acts, LV_SYMBOL_PLUS " Add", onNodeAction, NODE_ADD, true); + if (e.has_key) barButton(acts, LV_SYMBOL_LOOP " Ping", onNodeAction, NODE_PING); + if (pos) barButton(acts, UI_SYMBOL_COMPASS " Navigate", onNodeAction, NODE_NAV); + if (!lv_obj_get_child_count(acts)) lv_obj_delete(acts); + _node_ping = label(info, "", THEME_FONT_BODY, theme::ACCENT); // under the buttons, once there's a ping + lv_obj_add_flag(_node_ping, LV_OBJ_FLAG_HIDDEN); + nodeVizBuild(info); nodeviz::s_bearing = nodeviz::s_hops = -2; // nothing drawn yet: the first set fills both _node_info = infoCard(info); @@ -337,19 +339,6 @@ void UITask::buildNode() { s_nd_heard = infoRow(_node_info, "Heard", ""); s_nd_signal = infoRow(_node_info, "Signal", ""); s_nd_id = infoRow(_node_info, "ID", ""); - _node_ping = label(info, "", THEME_FONT_BODY, theme::ACCENT); - - // The bar: what the node is for -- a message, its admin, adding it -- a - // ping and the way there. The rest, less often, in a group under the info. - lv_obj_t* acts = buttonBar(body); - bool contact = e.contact_idx >= 0; - bool admin = contact && (e.type == ADV_TYPE_REPEATER || e.type == ADV_TYPE_ROOM); - bool pos = e.lat_e6 != 0 || e.lon_e6 != 0; - if (contact && e.type == ADV_TYPE_CHAT) barButton(acts, LV_SYMBOL_ENVELOPE " Message", onNodeAction, NODE_MSG, true); - if (admin) barButton(acts, LV_SYMBOL_SETTINGS " Admin", onNodeAction, NODE_ADMIN, true); - if (!contact && e.has_key && !e.is_known) barButton(acts, LV_SYMBOL_PLUS " Add", onNodeAction, NODE_ADD, true); - if (e.has_key) barButton(acts, LV_SYMBOL_LOOP " Ping", onNodeAction, NODE_PING); - if (pos) barButton(acts, UI_SYMBOL_COMPASS " Navigate", onNodeAction, NODE_NAV); if (pos || contact) { lv_obj_t* g = group(info, "More"); @@ -449,10 +438,12 @@ void UITask::refreshNode() { if (!_core->ping.isActive() && (out || back || rtt)) { setTextFmt(_node_ping, "Ping %lu ms - SNR out %.1f back %.1f", (unsigned long)rtt, out / 4.0f, back / 4.0f); + lv_obj_remove_flag(_node_ping, LV_OBJ_FLAG_HIDDEN); _pinging = false; } else if (millis() - _ping_started_ms > _core->ping.timeoutMs()) { _core->ping.clear(); setText(_node_ping, "Ping: no reply"); + lv_obj_remove_flag(_node_ping, LV_OBJ_FLAG_HIDDEN); _pinging = false; } } @@ -467,6 +458,7 @@ void UITask::nodeAction(uint8_t action) { case NODE_PING: { if (_pinging) break; PingEngine::StartResult r = _core->ping.start(e.pub_key); + lv_obj_remove_flag(_node_ping, LV_OBJ_FLAG_HIDDEN); if (r == PingEngine::STARTED) { _pinging = true; _ping_started_ms = millis(); diff --git a/examples/companion_radio/ui-lvgl/QuickPanel.h b/examples/companion_radio/ui-lvgl/QuickPanel.h index 0e9ebe43..6018e8af 100644 --- a/examples/companion_radio/ui-lvgl/QuickPanel.h +++ b/examples/companion_radio/ui-lvgl/QuickPanel.h @@ -43,6 +43,8 @@ static void hidden(lv_anim_t* a) { (void)a; lv_obj_add_flag(s_panel, LV_OBJ_FLAG_HIDDEN); if (s_catch) { lv_obj_delete(s_catch); s_catch = nullptr; } // the frozen screen goes with it + s_sb_panel = false; // back under the title bar, if the screen has one + s_ui->statusBarSync(); } static void slide(int32_t to, bool away) { lv_anim_delete(s_panel, NULL); @@ -134,7 +136,7 @@ static void onStatusBarTouch(lv_event_t* e) { s_drag = false; if (dy > s_h / 3) { s_open = true; slide(0, false); } else s_ui->quickPanelClose(true); - } else if (c == LV_EVENT_RELEASED && dy < 6) { + } else if (c == LV_EVENT_RELEASED && dy < 6 && !lv_event_get_user_data(e)) { // (a title bar: drags only) if (s_open) s_ui->quickPanelClose(true); else if (s_ui->quickPanelBegin()) { s_open = true; slide(0, false); } } @@ -256,6 +258,8 @@ bool UITask::quickPanelBegin() { freeze::early(s_catch, LV_OPA_50); // no memory, or a popup's still frozen: the screen stays as it is } lv_obj_remove_flag(s_panel, LV_OBJ_FLAG_HIDDEN); + s_sb_panel = true; // the full status bar over it, a title bar or not + statusBarSync(); lv_obj_scroll_to_x(s_pages, 0, LV_ANIM_OFF); // the first page each time dotsSet(0); layoutNow(s_panel); diff --git a/examples/companion_radio/ui-lvgl/QuickScreen.h b/examples/companion_radio/ui-lvgl/QuickScreen.h index 3566d5d9..480787a5 100644 --- a/examples/companion_radio/ui-lvgl/QuickScreen.h +++ b/examples/companion_radio/ui-lvgl/QuickScreen.h @@ -30,7 +30,7 @@ static lv_obj_t* chips(lv_obj_t* parent, lv_event_cb_t cb, bool wrap) { lv_obj_set_size(b, LV_SIZE_CONTENT, 28); lv_obj_set_style_pad_hor(b, 8, 0); lv_obj_set_style_pad_ver(b, 0, 0); - lv_obj_set_style_radius(b, theme::RADIUS_SM, 0); + lv_obj_set_style_radius(b, theme::RADIUS, 0); lv_obj_set_style_shadow_width(b, 0, 0); lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 0); lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, (void*)(uintptr_t)i); diff --git a/examples/companion_radio/ui-lvgl/RouteScreen.h b/examples/companion_radio/ui-lvgl/RouteScreen.h index 2d57d1fd..9fde1db9 100644 --- a/examples/companion_radio/ui-lvgl/RouteScreen.h +++ b/examples/companion_radio/ui-lvgl/RouteScreen.h @@ -53,7 +53,7 @@ static lv_obj_t* routeSquare(lv_obj_t* row, const char* sym, int code) { lv_obj_t* b = lv_button_create(row); lv_obj_set_size(b, 30, 26); lv_obj_set_style_pad_all(b, 0, 0); - lv_obj_set_style_radius(b, theme::RADIUS_SM, 0); + lv_obj_set_style_radius(b, theme::RADIUS, 0); lv_obj_set_style_shadow_width(b, 0, 0); lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 0); lv_obj_add_event_cb(b, onRouteOp, LV_EVENT_CLICKED, (void*)(uintptr_t)code); @@ -70,7 +70,7 @@ static lv_obj_t* routeRow(lv_obj_t* list, int h, const char* num, const char* na styleSurface(r, theme::SURFACE); lv_obj_remove_flag(r, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_size(r, LV_PCT(100), h); - lv_obj_set_style_radius(r, theme::RADIUS_SM, 0); + lv_obj_set_style_radius(r, theme::RADIUS, 0); lv_obj_set_style_pad_hor(r, 8, 0); lv_obj_set_style_pad_ver(r, 0, 0); lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW); diff --git a/examples/companion_radio/ui-lvgl/SoundScreen.h b/examples/companion_radio/ui-lvgl/SoundScreen.h index 547cd8cb..eeb11d35 100644 --- a/examples/companion_radio/ui-lvgl/SoundScreen.h +++ b/examples/companion_radio/ui-lvgl/SoundScreen.h @@ -99,7 +99,7 @@ static void rebuildStrip() { lv_obj_t* c = lv_button_create(s_strip); lv_obj_set_size(c, 38, 44); lv_obj_set_style_pad_all(c, 0, 0); - lv_obj_set_style_radius(c, theme::RADIUS_SM, 0); + lv_obj_set_style_radius(c, theme::RADIUS, 0); lv_obj_set_style_shadow_width(c, 0, 0); lv_obj_set_style_border_color(c, lv_color_hex(theme::ACCENT), 0); lv_obj_add_event_cb(c, onMelNote, LV_EVENT_CLICKED, (void*)(uintptr_t)i); @@ -144,7 +144,7 @@ static lv_obj_t* smallButton(lv_obj_t* parent, const char* text, uint8_t act) { lv_obj_t* b = lv_button_create(parent); lv_obj_set_size(b, 40, 32); lv_obj_set_style_pad_all(b, 0, 0); - lv_obj_set_style_radius(b, theme::RADIUS_SM, 0); + lv_obj_set_style_radius(b, theme::RADIUS, 0); lv_obj_set_style_shadow_width(b, 0, 0); lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 0); lv_obj_add_event_cb(b, onMelAction, LV_EVENT_CLICKED, (void*)(uintptr_t)act); diff --git a/examples/companion_radio/ui-lvgl/Theme.h b/examples/companion_radio/ui-lvgl/Theme.h index 49de9063..9f0d4ffa 100644 --- a/examples/companion_radio/ui-lvgl/Theme.h +++ b/examples/companion_radio/ui-lvgl/Theme.h @@ -27,10 +27,15 @@ namespace theme { // Spacing (px) static const int PAD = 8; static const int GAP = 6; - static const int RADIUS = 10; // cards, rows, buttons (pills use LV_RADIUS_CIRCLE) - static const int RADIUS_SM = 6; // small cells inside those - static const int STATUS_H = 22; // top status bar + static const int RADIUS = 10; // cards, rows and everything tapped: buttons, tabs, choices, chips + static const int RADIUS_SM = 6; // small drawn cells inside those (chart backgrounds, captions) + static const int STATUS_H = 22; // top status bar (Home, the map: screens without a title bar) static const int ROW_H = 44; // list row (touch target) + // A screen's title bar, at the top in place of the status bar: back, the + // title, the screen's actions, then the time and battery. + static const int APPBAR_H = 40; + static const int BAR_BTN_H = 32; // back and the actions in it + static const int TAB_H = 34; // a screen's tabs (segmented), the first thing under the title bar // Type -- Noto Sans, generated by fonts/generate.sh (declared in lv_conf.h). // ui_font_40 holds digits / ':' / '-' / '.' only (clock, timer, stopwatch). diff --git a/examples/companion_radio/ui-lvgl/ThreadView.h b/examples/companion_radio/ui-lvgl/ThreadView.h index 4de304c6..c77e4c9b 100644 --- a/examples/companion_radio/ui-lvgl/ThreadView.h +++ b/examples/companion_radio/ui-lvgl/ThreadView.h @@ -64,9 +64,10 @@ void UITask::setKeyboardVisible(bool show) { fieldFocus(_compose_ta, false); } if (_header && _body) { - int top = theme::STATUS_H + (show ? 0 : lv_obj_get_height(_header)); + int top = show ? theme::STATUS_H : theme::APPBAR_H; // typing: the slimmer status bar in its place if (show) lv_obj_add_flag(_header, LV_OBJ_FLAG_HIDDEN); else lv_obj_remove_flag(_header, LV_OBJ_FLAG_HIDDEN); + statusBarSync(); lv_obj_set_pos(_body, 0, top); lv_obj_set_height(_body, lv_display_get_vertical_resolution(NULL) - top); } @@ -139,7 +140,6 @@ void UITask::buildThread() { lv_obj_set_style_pad_all(body, 0, 0); lv_obj_set_style_pad_row(body, 0, 0); if (_header && (_thread_is_channel || can_send)) { // channel / conversation options - lv_obj_set_width(lv_obj_get_child(_header, 1), 200); // title, clear of the button headerButton(_header, LV_SYMBOL_SETTINGS, _thread_is_channel ? onChanThreadMenu : onConvThreadMenu, 4, NULL); } @@ -286,7 +286,7 @@ static void msgLocButton(lv_obj_t* parent, const char* text, bool save, bool acc lv_obj_set_size(b, LV_SIZE_CONTENT, 28); lv_obj_set_style_pad_hor(b, 10, 0); lv_obj_set_style_pad_ver(b, 0, 0); - lv_obj_set_style_radius(b, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_radius(b, theme::RADIUS, 0); lv_obj_set_style_shadow_width(b, 0, 0); // on our own amber bubble the main one goes dark instead lv_obj_set_style_bg_color(b, lv_color_hex(accent ? (own ? theme::BG : theme::ACCENT) : theme::SURFACE_2), 0); diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index 236a269f..4939755d 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -560,6 +560,7 @@ static void bannerShow(const char* icon, const char* title, const char* text) { lv_anim_delete(s_banner, NULL); lv_obj_remove_flag(s_banner, LV_OBJ_FLAG_HIDDEN); if (!shown) { // a new one while it's up just changes the text + lv_obj_align(s_banner, LV_ALIGN_TOP_MID, 0, belowTopBar() + 2); layoutNow(s_banner); anim::run(s_banner, anim::setTy, -(lv_obj_get_height(s_banner) + theme::STATUS_H), 0, anim::SCREEN_MS); } else { @@ -715,8 +716,20 @@ void UITask::refreshStatusBar() { default: break; } 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 + setText(_hdr_time, lv_label_get_text(_status_time)); + setTextFmt(_hdr_batt, "%s%s", batt, level); + 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 (_board->isExternalPowered()) { + if (chg) { lv_obj_remove_flag(_status_chg, LV_OBJ_FLAG_HIDDEN); placeLeftOf(_status_chg, _status_batt, -2); left_of = _status_chg; @@ -760,6 +773,16 @@ void UITask::refreshStatusBar() { placeLeftOf(_status_icons, left_of, -5); } +// The status bar is up over a screen without a title bar (Home, the map, a +// conversation typing), and over the quick panel and the lock whatever the +// screen; a title bar carries the time and battery itself. +void UITask::statusBarSync() { + if (!s_status_bar) return; + const bool titled = _header && !lv_obj_has_flag(_header, LV_OBJ_FLAG_HIDDEN); + const bool hide = titled && !s_sb_panel && !s_sb_lock; + if (hide != lv_obj_has_flag(s_status_bar, LV_OBJ_FLAG_HIDDEN)) lv_obj_set_flag(s_status_bar, LV_OBJ_FLAG_HIDDEN, hide); +} + void UITask::setGps(bool on) { if (!_core->setGpsEnabled(on, false)) { showToast("No GPS on this device"); return; } prefsSave(); // written on leaving the screen, as the other switches @@ -895,8 +918,8 @@ void UITask::showToast(const char* text, uint32_t ms) { bool shown = !lv_obj_has_flag(_toast, LV_OBJ_FLAG_HIDDEN); lv_anim_delete(_toast, NULL); // on its way out lv_obj_remove_flag(_toast, LV_OBJ_FLAG_HIDDEN); - // Under the message banner while that's up, else right under the status bar. - int32_t y = theme::STATUS_H + 4; + // Under the message banner while that's up, else right under the top bar. + int32_t y = belowTopBar() + 4; if (s_banner && !lv_obj_has_flag(s_banner, LV_OBJ_FLAG_HIDDEN)) y = lv_obj_get_y(s_banner) + lv_obj_get_height(s_banner) + 4; lv_obj_align(_toast, LV_ALIGN_TOP_MID, 0, y); layoutNow(_toast); @@ -990,7 +1013,7 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) { if (_shown_screen == SCR_SETTINGS && _body) s_settings_y = lv_obj_get_scroll_y(_body); pickerClose(); _header = _body = nullptr; - _nearby_list = _nearby_status = _nearby_sort_lbl = _nearby_chips = nullptr; + _nearby_list = _nearby_status = nullptr; _node_info = _node_ping = _node_delete_lbl = nullptr; nodeviz::s_card = nullptr; _scan_overlay = _scan_list = _scan_status = nullptr; // the popup went with the old screen @@ -1012,18 +1035,30 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) { styleOpaque(scr, theme::BG); lv_obj_remove_flag(scr, LV_OBJ_FLAG_SCROLLABLE); + // The title bar, at the very top in place of the status bar: back, the + // title, the screen's actions (headerButton(), after the title in a row), + // 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; if (title) { 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), 38); - lv_obj_set_pos(hdr, 0, top); + lv_obj_set_size(hdr, LV_PCT(100), theme::APPBAR_H); + lv_obj_set_pos(hdr, 0, 0); + lv_obj_set_flex_flow(hdr, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(hdr, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_left(hdr, with_back ? 4 : theme::PAD, 0); + lv_obj_set_style_pad_column(hdr, theme::GAP, 0); + lv_obj_add_flag(hdr, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(hdr, LV_OBJ_FLAG_PRESS_LOCK); + lv_obj_remove_flag(hdr, LV_OBJ_FLAG_GESTURE_BUBBLE); + lv_obj_add_event_cb(hdr, onStatusBarTouch, LV_EVENT_ALL, (void*)1); // a drag only: a tap on the title is nothing _header = hdr; if (with_back) { lv_obj_t* b = lv_button_create(hdr); - lv_obj_set_size(b, 40, 28); - lv_obj_align(b, LV_ALIGN_LEFT_MID, 4, 0); + lv_obj_set_size(b, 40, theme::BAR_BTN_H); lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE), 0); lv_obj_set_style_shadow_width(b, 0, 0); lv_obj_add_event_cb(b, onBack, LV_EVENT_CLICKED, NULL); @@ -1031,9 +1066,38 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) { } lv_obj_t* t = label(hdr, title, THEME_FONT_TITLE, theme::TEXT); 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 += 38; + lv_obj_set_width(t, 1); + lv_obj_set_flex_grow(t, 1); // what the actions leave + // 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); + lv_obj_remove_style_all(c); + lv_obj_add_flag(c, LV_OBJ_FLAG_IGNORE_LAYOUT); + lv_obj_add_flag(c, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(c, LV_OBJ_FLAG_PRESS_LOCK); + lv_obj_remove_flag(c, LV_OBJ_FLAG_GESTURE_BUBBLE); + lv_obj_add_event_cb(c, onStatusBarTouch, LV_EVENT_ALL, NULL); // a tap opens the panel, as the status bar's + lv_obj_set_size(c, LV_SIZE_CONTENT, LV_PCT(100)); + lv_obj_set_style_pad_left(c, theme::GAP, 0); + lv_obj_set_style_pad_right(c, 4, 0); + lv_obj_set_flex_flow(c, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(c, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_align(c, LV_ALIGN_RIGHT_MID, 0, 0); + _hdr_clock = c; + lv_obj_t* p = lv_obj_create(c); + lv_obj_remove_style_all(p); + lv_obj_remove_flag(p, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_size(p, LV_SIZE_CONTENT, theme::BAR_BTN_H); + lv_obj_set_flex_flow(p, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(p, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(p, 6, 0); + lv_obj_set_style_bg_color(p, lv_color_hex(theme::SURFACE), 0); + lv_obj_set_style_bg_opa(p, LV_OPA_COVER, 0); + lv_obj_set_style_radius(p, theme::RADIUS, 0); + lv_obj_set_style_pad_hor(p, 8, 0); + _hdr_time = label(p, "", THEME_FONT_BODY, theme::TEXT); + _hdr_batt = label(p, "", THEME_FONT_ICONS, theme::TEXT_MUTED); + top = theme::APPBAR_H; } lv_obj_t* body = lv_obj_create(scr); @@ -1063,6 +1127,8 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) { if (prev && !same && !_asleep) anim::screenIn(body, backward); } _fade_next = false; + statusBarSync(); + refreshStatusBar(); // the title bar's time and battery return body; } @@ -1103,8 +1169,12 @@ void UITask::back() { case SCR_SETTINGS: if (_nav_overlay) navClosePopup(); else showHome(); break; case SCR_SETTINGS_NAV: // the map's options go back to the map if (_settings_page == settings::PG_NAV && s_opts_from_map) { - openMap(true); - if (s_nav_section >= 0) navToolsPopup(); // one section's options: back into Map tools + // The map as it was left -- its mode and view, Map tools over it, + // scrolled where it was -- dissolving in over the page. + const bool tools = s_nav_section >= 0; + _fade_next = true; + showMap(false); + if (tools) navToolsPopup(true); } else if (_settings_page == settings::PG_RADIO) { showRadio(); } else { @@ -1145,7 +1215,7 @@ void UITask::back() { else showHome(); break; case SCR_NODE: // back to where the node was picked: map, the list, or the scan popup over it - if (_node_from_map) { openMap(false); break; } + if (_node_from_map) { _map_nodes = true; showMap(false); break; } // the nodes map as it was left _screen = SCR_NEARBY; buildNearby(); if (_node_from_scan) showScanPopup(); @@ -1355,3 +1425,36 @@ extern "C" EMSCRIPTEN_KEEPALIVE int sim_scroll(int dy) { return left; } #endif + +#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__) +// The simulator page opens a screen by name (screenshots of the layout): +// home, chats, contacts, settings, settings:, nearby, map, navmap, places, clock, radio, +// gps, diag, compass, storage, repeater, wifi, bot, quick, node, channel. +extern "C" EMSCRIPTEN_KEEPALIVE int sim_ui_show(const char* name) { + if (!s_ui || !name) return 0; + UITask& u = *s_ui; + if (!strcmp(name, "home")) u.showHome(); + else if (!strcmp(name, "chats")) u.showChats(); + else if (!strcmp(name, "contacts")) u.showContacts(); + else if (!strcmp(name, "settings")) u.showSettings(); + else if (!strncmp(name, "settings:", 9)) u.showSchemaSettings(atoi(name + 9)); + else if (!strcmp(name, "nearby")) u.showNearby(); + else if (!strcmp(name, "map")) u.openMap(false); + else if (!strcmp(name, "navmap")) u.openMap(true); + else if (!strcmp(name, "places")) { u.openMap(true); u.navTargetsPopup(); } + else if (!strcmp(name, "clock")) u.showClock(); + else if (!strcmp(name, "radio")) u.showRadio(); + else if (!strcmp(name, "gps")) u.showGps(false); + else if (!strcmp(name, "diag")) u.showDiag(); + else if (!strcmp(name, "compass")) u.showCompass(); + else if (!strcmp(name, "storage")) u.showStorage(); + else if (!strcmp(name, "repeater")) u.showRepeater(); + else if (!strcmp(name, "wifi")) u.showWifi(); + else if (!strcmp(name, "bot")) u.showBot(); + else if (!strcmp(name, "quick")) u.showQuickMsgs(); + else if (!strcmp(name, "node")) { u.showNearby(); u.openNode(0); } + else if (!strcmp(name, "channel")) u.openChannel(0); + else return 0; + return 1; +} +#endif diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index 776f62b1..c6a6a747 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -105,7 +105,7 @@ public: void setSchemaValue(int idx, int v); void setMsgBanner(bool on); // Settings > Display > Message banner void showNearby(); - void showMap(); // in its current mode + void showMap(bool frame = true); // in its current mode; frame: on the target, if there is one void openMap(bool nav); // the one map; false: from Nodes, with the nodes on it and Back to Nodes void mapLongPress(int x, int y); void mapZoom(int delta); @@ -178,7 +178,7 @@ public: void navMarkHere(); void navAveragingCancel(); void navToNode(const uint8_t* key, int32_t lat, int32_t lon, const char* name); - void navToolsPopup(); + void navToolsPopup(bool back = false); // back: from a page it led to, as it was left void navToolAction(uint8_t act); void navSetShareTarget(int sel); void navWaypointsPopup(); @@ -197,7 +197,7 @@ public: void wifiEdit(lv_obj_t* ta); void wifiKeyboardHide(); void setNearbyFilter(uint8_t f); - void toggleNearbySort(); + void setNearbySort(bool last_heard); void startNearbyScan(); void closeScanPopup(); void openScanNode(int row); @@ -368,6 +368,7 @@ public: void openRouteFor(const uint8_t* pub_key) { showRoute(pub_key); } void openMessagesList() { showChats(); } // sim harness bool openNodesMap() { openMap(false); return true; } // sim harness + void statusBarSync(); // up or hidden, by the screen's title bar (UITask.cpp) private: enum Screen : uint8_t { SCR_HOME, SCR_CHATS, SCR_CONTACTS, SCR_THREAD, SCR_SETTINGS, SCR_NEARBY, SCR_NODE, SCR_MAP, SCR_WIFI, SCR_SETTINGS_NAV, SCR_CLOCK, SCR_RADIO, SCR_CHANNEL_EDIT, SCR_ADMIN, SCR_BOT, SCR_DIAG, SCR_COMPASS, SCR_SCOPES, SCR_REPEATER, SCR_MELODY, SCR_QUICK, SCR_ADMIN_PICK, SCR_OTA, SCR_STORAGE, SCR_GPS, SCR_BATT, SCR_ROUTE, SCR_ROUTE_PICK }; @@ -437,6 +438,8 @@ private: void addMapMark(uint8_t kind, int idx, int32_t lat_e6, int32_t lon_e6, uint32_t col, const char* text); void mapPointToLatLon(int x, int y, int32_t& lat_e6, int32_t& lon_e6) const; double mapCenterBias() const; + int mapBottomH() const; + void mapPlaceBottom(); void buildNavLayers(); void buildNavControls(lv_obj_t* body); void rebuildNavMarkers(); @@ -605,6 +608,9 @@ private: lv_obj_t* _home_date = nullptr; 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_time = nullptr; + lv_obj_t* _hdr_batt = nullptr; lv_obj_t* _body = nullptr; lv_obj_t* _thread_list = nullptr; lv_obj_t* _compose_ta = nullptr; @@ -627,8 +633,6 @@ private: uint32_t _ui_started_ms = 0; // begin(): the side button held soon after is the CLI rescue lv_obj_t* _nearby_list = nullptr; lv_obj_t* _nearby_status = nullptr; - lv_obj_t* _nearby_sort_lbl = nullptr; - lv_obj_t* _nearby_chips = nullptr; lv_obj_t* _node_info = nullptr; lv_obj_t* _node_ping = nullptr; lv_obj_t* _node_delete_lbl = nullptr; diff --git a/examples/companion_radio/ui-lvgl/Widgets.h b/examples/companion_radio/ui-lvgl/Widgets.h index b9a58f9f..163ee104 100644 --- a/examples/companion_radio/ui-lvgl/Widgets.h +++ b/examples/companion_radio/ui-lvgl/Widgets.h @@ -164,6 +164,20 @@ static lv_obj_t* s_status_bar = nullptr; // on the top layer, opaque static int32_t belowStatusBar() { return s_status_bar && !lv_obj_has_flag(s_status_bar, LV_OBJ_FLAG_HIDDEN) ? lv_obj_get_y2(s_status_bar) + 1 : 0; } +// Where what drops in from the top (a toast, the message banner) goes: under +// the status bar, or under a screen's title bar, which takes its place. +static int32_t belowTopBar() { + return s_status_bar && lv_obj_has_flag(s_status_bar, LV_OBJ_FLAG_HIDDEN) ? theme::APPBAR_H : theme::STATUS_H; +} +// Home's tiles: `rows` of them fill the page between the status bar and the +// page dots, less `reserve` px left under them (a hint line). +static const int HOME_DOTS_H = 26; +static int homeTileH(int rows, int reserve = 0) { + const int page = lv_display_get_vertical_resolution(NULL) - theme::STATUS_H - theme::PAD - HOME_DOTS_H; + return (page - reserve - (rows - 1) * theme::GAP) / rows; +} +// What keeps the status bar up over a title bar (UITask::statusBarSync). +static bool s_sb_panel = false, s_sb_lock = false; // The screen under an open popup, frozen: once the dim is up, the screen is // drawn once into an image, darkened as the dim layer darkens it, and shown @@ -245,7 +259,7 @@ namespace freeze { } static void dimmed(lv_obj_t* overlay, bool top) { // the image holds the dim now lv_obj_set_style_bg_opa(overlay, LV_OPA_TRANSP, 0); - if (top) { // the status bar, on the top layer too, stays dimmed + if (top && belowStatusBar()) { // the status bar, on the top layer too, stays dimmed s_strip = lv_obj_create(overlay); lv_obj_remove_style_all(s_strip); lv_obj_remove_flag(s_strip, LV_OBJ_FLAG_CLICKABLE); @@ -306,6 +320,9 @@ namespace freeze { // POP_BOTTOM: as tall as its content, at the bottom, nothing dimmed -- for // what's shown on the screen above it (a map area's preview). enum PopFit : uint8_t { POP_FIT, POP_FULL, POP_TOP, POP_BOTTOM }; +// The next popup is already up: back to it from a page it led to, it is +// there at once -- dimmed in full, no rise -- not opening again. +static bool s_pop_still = false; static lv_obj_t* popupOpen(lv_obj_t* parent, PopFit fit, lv_obj_t*& overlay, int32_t inset = 8) { overlay = dimOverlay(parent); s_fit_pending = true; // rowsFit() @@ -335,12 +352,15 @@ static lv_obj_t* popupOpen(lv_obj_t* parent, PopFit fit, lv_obj_t*& overlay, int lv_obj_set_style_pad_all(panel, theme::PAD, 0); lv_obj_set_style_pad_row(panel, 4, 0); lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN); + const bool still = s_pop_still; + s_pop_still = false; if (fit == POP_BOTTOM) { // over a live screen, nothing dimmed lv_obj_set_style_bg_opa(overlay, LV_OPA_TRANSP, 0); anim::rise(panel); } else { lv_obj_add_event_cb(overlay, freeze::ownerDeleted, LV_EVENT_DELETE, NULL); - if (freeze::early(overlay, LV_OPA_60)) anim::rise(panel); + if (still) { lv_obj_set_style_bg_opa(overlay, LV_OPA_60, 0); freeze::take(overlay); } // frozen as it looks + else if (freeze::early(overlay, LV_OPA_60)) anim::rise(panel); else anim::popup(overlay, LV_OPA_60, freeze::onDimUp); } return panel; @@ -470,7 +490,7 @@ static lv_obj_t* pillButton(lv_obj_t* row, const char* text, uint32_t bg, lv_eve lv_obj_set_size(b, LV_SIZE_CONTENT, 24); lv_obj_set_style_pad_hor(b, 10, 0); lv_obj_set_style_pad_ver(b, 0, 0); - lv_obj_set_style_radius(b, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_radius(b, theme::RADIUS, 0); lv_obj_set_style_shadow_width(b, 0, 0); lv_obj_set_style_bg_color(b, lv_color_hex(bg), 0); lv_obj_align(b, LV_ALIGN_RIGHT_MID, -2, 0); @@ -617,14 +637,15 @@ static void rowFit(lv_obj_t* row) { } } -// ... and every label given a width in pixels, which labelFit() then measures +// ... and every label given a width in pixels (not one that grows), which labelFit() then measures // at it (the screen titles, a list row's cut-off name ...). static void rowsFit(lv_obj_t* root) { lv_obj_tree_walk(root, [](lv_obj_t* o, void*) { if (lv_obj_has_flag(o, LV_OBJ_FLAG_USER_2)) { lv_obj_remove_flag(o, LV_OBJ_FLAG_USER_2); rowFit(o); - } else if (lv_obj_check_type(o, &lv_label_class)) { + } else if (lv_obj_check_type(o, &lv_label_class) && !lv_obj_get_style_flex_grow(o, LV_PART_MAIN)) { + // (a growing one's px width is a stand-in: the flex's, or its row's rowFit()) int32_t w = lv_obj_get_style_width(o, LV_PART_MAIN); if (LV_COORD_IS_PX(w) && w > 0 && lv_obj_get_width(o) != w) labelFit(o, w); } @@ -827,7 +848,7 @@ static lv_obj_t* choiceCreate(lv_obj_t* parent, const char* opts, int sel, const if (!isGroup(lv_obj_get_parent(parent))) { lv_obj_set_style_bg_color(c, lv_color_hex(theme::SURFACE_2), 0); lv_obj_set_style_bg_opa(c, LV_OPA_COVER, 0); - lv_obj_set_style_radius(c, theme::RADIUS_SM, 0); + lv_obj_set_style_radius(c, theme::RADIUS, 0); lv_obj_set_style_pad_hor(c, 10, 0); lv_obj_set_style_pad_ver(c, 8, 0); } @@ -853,6 +874,29 @@ static lv_obj_t* choiceCreate(lv_obj_t* parent, const char* opts, int sel, const return c; } +// A list's toolbar: one line over it, a muted text on the left (how many, +// what's missing) and its choices on the right (toolbarChoice). Returns the +// text, to update; its parent is the row. +static lv_obj_t* toolbarRow(lv_obj_t* parent) { + lv_obj_t* row = flexBox(parent, LV_FLEX_FLOW_ROW); + lv_obj_set_size(row, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_flex_align(row, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(row, theme::GAP, 0); + lv_obj_t* t = label(row, "", THEME_FONT_SMALL, theme::TEXT_MUTED); + lv_label_set_long_mode(t, LV_LABEL_LONG_DOT); + lv_obj_set_width(t, 1); + lv_obj_set_flex_grow(t, 1); + return t; +} +// A choice in a toolbarRow(): the value and a chevron on a small surface, +// the options over the screen as any choice's. +static lv_obj_t* toolbarChoice(lv_obj_t* row, const char* opts, int sel, const char* title) { + lv_obj_t* c = choiceCreate(row, opts, sel, title); + lv_obj_set_style_pad_ver(c, 6, 0); + lv_obj_set_style_pad_hor(c, 8, 0); + lv_obj_set_style_text_font(choiceOf(c)->value, THEME_FONT_SMALL, 0); + return c; +} // One settings row: label left, a choice right. static lv_obj_t* choiceRow(lv_obj_t* parent, const char* text, const char* hint, const char* options, int sel, @@ -931,7 +975,7 @@ static lv_obj_t* segmented(lv_obj_t* parent, const char** map, int sel, int w, i lv_obj_set_style_text_color(m, lv_color_hex(theme::BG), LV_PART_ITEMS | LV_STATE_CHECKED); lv_obj_set_style_text_font(m, THEME_FONT_SMALL, LV_PART_ITEMS); lv_obj_set_style_shadow_width(m, 0, LV_PART_ITEMS); - lv_obj_set_style_radius(m, theme::RADIUS_SM, LV_PART_ITEMS); + lv_obj_set_style_radius(m, theme::RADIUS, LV_PART_ITEMS); return m; }