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 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-14 12:51:03 +02:00
co-authored by Claude Opus 5.5
parent a8440c890e
commit c779bf46b5
23 changed files with 408 additions and 244 deletions
+5
View File
@@ -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:
+7 -4
View File
@@ -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
@@ -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);
+2 -2
View File
@@ -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);
+18 -17
View File
@@ -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 = {
+5 -14
View File
@@ -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);
@@ -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 ─────────────────────────────────────────────────────────────────────
@@ -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);
@@ -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);
+2 -3
View File
@@ -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);
}
@@ -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);
+29 -10
View File
@@ -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 };
+83 -96
View File
@@ -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() {
+41 -49
View File
@@ -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();
@@ -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);
@@ -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);
@@ -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);
@@ -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);
+8 -3
View File
@@ -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).
@@ -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);
+117 -14
View File
@@ -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:<page>, 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
+9 -5
View File
@@ -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;
+51 -7
View File
@@ -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;
}