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 ♦ by distance or by last heard, is in the options. A ★ marks a favourite, a ♦
someone sharing their position live. 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, **Enter** shows a node's details. **Hold Enter**, in the list or the details,
for what you can do with it: 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**. contact or channel) and **Set as target**.
> [!NOTE] > [!NOTE]
> **Wio Tracker L2:** up to 64 waypoints. The pin button on the map opens the > **Wio Tracker L2:** up to 64 waypoints. The pin button on the map opens
> list; hold a spot on the map to make it a waypoint or the target. > **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 ## Navigating to something
@@ -73,8 +75,9 @@ degrees and direction.
> [!NOTE] > [!NOTE]
> **Wio Tracker L2:** the target is drawn on the map with a line to it, and a > **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 > bar along the bottom shows distance, bearing, heading and time to arrival
> a turning dial. > (without a target the map has the whole screen). The Compass app is a
> turning dial.
## Sharing locations ## Sharing locations
@@ -171,7 +171,7 @@ void UITask::buildAdmin() {
static const char* TABS[admin::TAB_COUNT + 1]; static const char* TABS[admin::TAB_COUNT + 1];
for (int i = 0; i < admin::TAB_COUNT; i++) TABS[i] = admin::TAB_LABELS[i]; for (int i = 0; i < admin::TAB_COUNT; i++) TABS[i] = admin::TAB_LABELS[i];
TABS[admin::TAB_COUNT] = ""; 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); lv_obj_add_event_cb(s_tabs, onAdminTab, LV_EVENT_VALUE_CHANGED, NULL);
s_status = label(body, "", THEME_FONT_SMALL, theme::ACCENT); 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_height(b, 26);
lv_obj_set_style_pad_hor(b, 7, 0); lv_obj_set_style_pad_hor(b, 7, 0);
lv_obj_set_style_pad_ver(b, 0, 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_shadow_width(b, 0, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 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); 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]; static const char* TABS[botcfg::TAB_COUNT + 1];
for (int i = 0; i < botcfg::TAB_COUNT; i++) TABS[i] = botcfg::TAB_LABELS[i]; for (int i = 0; i < botcfg::TAB_COUNT; i++) TABS[i] = botcfg::TAB_LABELS[i];
TABS[botcfg::TAB_COUNT] = ""; 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_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); 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), // Messages section title: a chevron and the title fold the section (tap),
// an "All" / "★ Fav" pill on the right (filter >= 0) flips its // an All / Favourites choice on the right (filter >= 0), as the Nodes list's
// favourites-only filter. Folded, the title carries the unread count. // toolbar has. Folded, the title carries the unread count.
enum : uint8_t { FOLD_CHANNELS, FOLD_DIRECT, FOLD_ROOMS }; 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 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)); } 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); 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) { static void chatSection(lv_obj_t* parent, const char* text, int fold, int unread, int filter, bool fav_only) {
bool folded = chatFolded(fold); bool folded = chatFolded(fold);
lv_obj_t* row = lv_obj_create(parent); 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); lv_obj_align_to(n, t, LV_ALIGN_OUT_RIGHT_MID, 8, 0);
} }
if (filter < 0 || folded) return; 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 title with the All / Favourites choice on the right.
// section's favourites-only filter.
static void sectionWithFilter(lv_obj_t* parent, const char* text, bool fav_only, uint8_t which) { static void sectionWithFilter(lv_obj_t* parent, const char* text, bool fav_only, uint8_t which) {
lv_obj_t* row = lv_obj_create(parent); lv_obj_t* row = lv_obj_create(parent);
styleSurface(row, theme::BG); styleSurface(row, theme::BG);
lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE); lv_obj_remove_flag(row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(row, LV_PCT(100), 26); lv_obj_set_size(row, LV_PCT(100), 30);
lv_obj_align(label(row, text, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_BOTTOM_LEFT, 0, -2); lv_obj_align(label(row, text, THEME_FONT_SMALL, theme::TEXT_MUTED), LV_ALIGN_LEFT_MID, 0, 0);
styleSelected(pillButton(row, fav_only ? UI_SYMBOL_STAR " Fav" : "All", theme::SURFACE, onChatFilter, which), fav_only); chatFilterChoice(row, which, fav_only);
} }
void UITask::showChats() { 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() { void UITask::buildChats() {
lv_obj_t* body = newScreen("Messages", true); lv_obj_t* body = newScreen("Messages", true);
s_chats_read_all = unreadTotal() > 0; s_chats_read_all = unreadTotal() > 0;
if (_header) { // a new message first, whatever the lists below hold if (_header) { // a new message last, at the edge, whatever the lists below hold
lv_obj_t* nb = headerButton(_header, LV_SYMBOL_EDIT " New", onNewChat, 4, NULL); if (s_chats_read_all) headerButton(_header, LV_SYMBOL_OK " Read all", onMarkAllRead, 0, NULL);
stylePrimary(nb); stylePrimary(headerButton(_header, LV_SYMBOL_EDIT, onNewChat, 0, NULL)); // the pencil alone: the title fits
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);
}
} }
vlist::attach(body); vlist::attach(body);
fillChats(); fillChats();
@@ -281,7 +282,7 @@ void UITask::fillChats() {
vlist::add(body, &DM, rows); vlist::add(body, &DM, rows);
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 // Room servers: tap logs in (saved password or ask) and opens; hold: options
static const vlist::Kind ROOM = { 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 }; *w = { n, (int32_t)(sel % n) * 256, 0, 0 };
lv_obj_t* o = lv_obj_create(parent); lv_obj_t* o = lv_obj_create(parent);
styleSurface(o, theme::SURFACE); 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_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_size(o, 60, 84); // three rows; leaves room for the button below
lv_obj_set_user_data(o, w); 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 } // namespace clockview
static void onOpenClock(lv_event_t* e) { (void)e; s_ui->showClock(); } 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 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(); } 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; s_big = s_go_lbl = s_alarm_sw = s_cal = nullptr;
// Tabs // Tabs
lv_obj_t* tabs = centerRow(body); static const char* TABS[TAB_COUNT + 1] = { "Alarm", "Timer", "Stopwatch", "Calendar", "" };
static const char* const NAMES[TAB_COUNT] = { "Alarm", "Timer", "Stopwatch", "Calendar" }; lv_obj_t* tabs = segmented(body, TABS, s_tab, LV_PCT(100), theme::TAB_H, theme::SURFACE);
for (int t = 0; t < TAB_COUNT; t++) { lv_obj_add_event_cb(tabs, onClockTab, LV_EVENT_VALUE_CHANGED, NULL);
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);
}
if (s_tab == TAB_ALARM) { if (s_tab == TAB_ALARM) {
lv_obj_t* r = centerRow(body); lv_obj_t* r = centerRow(body);
@@ -90,7 +90,10 @@ static void onMsgHold(lv_event_t* e) {
lv_indev_wait_release(lv_indev_active()); lv_indev_wait_release(lv_indev_active());
s_ui->messageMenu(threadRowOf((lv_obj_t*)lv_event_get_current_target(e))); 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 ───────────────────────────────────────────────────────────────────── // ── Rooms ─────────────────────────────────────────────────────────────────────
@@ -147,6 +147,8 @@ void UITask::lockScreen() {
lv_obj_remove_style_all(s_lock); 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_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 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_color(s_lock, lv_color_hex(theme::BG), 0);
lv_obj_set_style_bg_opa(s_lock, LV_OPA_COVER, 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 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; 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'; _pin_entry[0] = '\0';
s_sb_lock = false;
statusBarSync();
if (_screen == SCR_HOME) refreshHome(); if (_screen == SCR_HOME) refreshHome();
else if (_screen == SCR_CHATS) refreshChats(); // counts moved on while locked else if (_screen == SCR_CHATS) refreshChats(); // counts moved on while locked
else if (_screen == SCR_THREAD) reopenThreadIfUnread(); // on to what came in meanwhile 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 used = contactctl::favName(_prefs, s, name, sizeof(name), &c);
bool chan = favslots::kind(_prefs, s) == NodePrefs::FAV_KIND_CHANNEL; bool chan = favslots::kind(_prefs, s) == NodePrefs::FAV_KIND_CHANNEL;
lv_obj_t* b = lv_button_create(grid); 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_pad_all(b, 4, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0); lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_set_style_shadow_width(b, 0, 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, onFavTap, LV_EVENT_CLICKED, (void*)(uintptr_t)s);
lv_obj_add_event_cb(b, onFavHold, LV_EVENT_LONG_PRESSED, (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; 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_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_label_set_long_mode(n, LV_LABEL_LONG_DOT);
lv_obj_set_style_text_align(n, LV_TEXT_ALIGN_CENTER, 0); lv_obj_set_style_text_align(n, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_width(n, w - 10); 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) { if (used) {
int unread = chan ? _core->history.chUnread(_prefs->favourite_contacts[s][0]) int unread = chan ? _core->history.chUnread(_prefs->favourite_contacts[s][0])
: (c.type == ADV_TYPE_CHAT ? _core->dmUnread(c.id.pub_key) : 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); if (s_tab == TAB_LIVE && _header) headerButton(_header, LV_SYMBOL_REFRESH " Reset", onDiagReset, 4, NULL);
static const char* TABS[] = { "Live", "History", "System", "Font", "Noise", "" }; 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_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); 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; 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; for (lv_obj_t*& p : s_sys_pill) p = nullptr;
lv_obj_set_style_pad_row(body, 6, 0); 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", "" }; static const char* TABS[] = { "Sky", "Signal", "Details", "" };
lv_obj_t* tabs = segmented(_header, TABS, s_tab, 186, 28, theme::SURFACE); lv_obj_t* tabs = segmented(body, TABS, s_tab, LV_PCT(100), theme::TAB_H, theme::SURFACE);
lv_obj_align(tabs, LV_ALIGN_RIGHT_MID, -4, 0);
lv_obj_add_event_cb(tabs, onGpsTab, LV_EVENT_VALUE_CHANGED, NULL); 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) { 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; 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_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_pad_all(b, 4, 0);
lv_obj_set_style_radius(b, theme::RADIUS, 0); lv_obj_set_style_radius(b, theme::RADIUS, 0);
lv_obj_set_style_shadow_width(b, 0, 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), 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); 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); 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_obj_t* n = label(b, text, THEME_FONT_SMALL, theme::TEXT);
lv_label_set_long_mode(n, LV_LABEL_LONG_DOT); 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_style_text_align(n, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_set_width(n, w - 10); 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); tileBadge(b, badge_n);
return b; return b;
} }
@@ -441,7 +441,7 @@ void UITask::goHome() {
void UITask::buildHome() { void UITask::buildHome() {
lv_obj_t* body = newScreen(NULL, false); 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_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); home::s_page_box = lv_obj_create(body);
styleSurface(home::s_page_box, theme::BG); styleSurface(home::s_page_box, theme::BG);
lv_obj_remove_flag(home::s_page_box, LV_OBJ_FLAG_SCROLLABLE); 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_add_flag(b, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_size(b, LV_SIZE_CONTENT, 26); lv_obj_set_size(b, LV_SIZE_CONTENT, 26);
lv_obj_set_style_pad_hor(b, 14, 0); 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_shadow_width(b, 0, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::ACCENT), 0); lv_obj_set_style_bg_color(b, lv_color_hex(theme::ACCENT), 0);
lv_obj_align(b, LV_ALIGN_BOTTOM_RIGHT, 0, 24); 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 mapview
namespace navmap { // shared with NavMap.h (included after this file) 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. // Navigation targets as one int (event user data): type << 8 | index.
enum : uint8_t { T_CLEAR, T_WAYPOINT, T_TRAILSTART, T_LIVE }; enum : uint8_t { T_CLEAR, T_WAYPOINT, T_TRAILSTART, T_LIVE };
static int code(uint8_t type, int idx) { return (type << 8) | (idx & 0xFF); } 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 // 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). // on the Navigation map (the bias is in tiles, added to the view centre).
double UITask::mapCenterBias() const { 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 #ifdef UI_PERF_TEST
@@ -615,7 +633,7 @@ void UITask::openMap(bool nav) {
showMap(); showMap();
} }
void UITask::showMap() { void UITask::showMap(bool frame) {
_screen = SCR_MAP; _screen = SCR_MAP;
mapview::s_trails_on = lvport::trailsOn(); mapview::s_trails_on = lvport::trailsOn();
mapview::s_provider = lvport::vectorOn() ? (mapview::TileProvider*)&mapview::s_vector : &mapview::s_raster; 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); _map_cy = mapview::latToTileY(la, z);
} }
buildMap(); 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() { void UITask::buildMap() {
@@ -713,9 +731,8 @@ void UITask::buildMap() {
// left column (back, pin, tools) -- the right one has no room above the bar. // 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); 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); 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 navmap::s_ready = false;
_map_center_btn = mapButton(body, LV_SYMBOL_GPS, onMapCenter); _map_center_btn = mapButton(body, LV_SYMBOL_GPS, onMapCenter); // bottom right (mapPlaceBottom)
lv_obj_align(_map_center_btn, LV_ALIGN_BOTTOM_RIGHT, -6, -6 - bottom);
// Top centre: what's going on, one pill under another -- the trail / // Top centre: what's going on, one pill under another -- the trail /
// live share (NavMap.h), the download. Empty most of the time. // live share (NavMap.h), the download. Empty most of the time.
mapview::s_going = lv_obj_create(body); 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); 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); mapview::s_scale_lbl = label(sc, "", THEME_FONT_SMALL, theme::TEXT);
_map_zoom_lbl = label(sc, "", THEME_FONT_SMALL, theme::TEXT_MUTED); _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, ""); _map_hint = mapPill(body, "");
lv_label_set_long_mode(_map_hint, LV_LABEL_LONG_WRAP); lv_label_set_long_mode(_map_hint, LV_LABEL_LONG_WRAP);
lv_obj_set_width(_map_hint, 200); lv_obj_set_width(_map_hint, 200);
@@ -772,11 +789,13 @@ void UITask::buildMap() {
if (_map_nav) buildNavControls(body); if (_map_nav) buildNavControls(body);
_route_bar = _route_lbl = nullptr; _route_bar = _route_lbl = nullptr;
if (_route_pick) buildRouteBar(body); if (_route_pick) buildRouteBar(body);
mapPlaceBottom();
layoutNow(body); layoutNow(body);
mapview::s_mark_count = 0; // the previous markers went with the previous screen mapview::s_mark_count = 0; // the previous markers went with the previous screen
rebuildMapMarkers(); rebuildMapMarkers();
layoutMap(); layoutMap();
navmap::s_ready = true;
} }
// Places tiles and markers for the current centre / zoom; queues tiles that // 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::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(), 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 // Own position
int32_t lat, lon; 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; }; 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 static Box used[6 + MAX_MARKS + MARK_LABELS_MAX]; // the map's controls, the dots, the names
int nu = 0; 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); const int16_t W = (int16_t)w, H = (int16_t)(h - bottom);
used[nu++] = { 0, 0, 50, 144 }; // button columns used[nu++] = { 0, 0, 50, 144 }; // button columns
used[nu++] = { (int16_t)(W - 50), 0, W, 144 }; 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_hor(_nav_bar, theme::PAD, 0);
lv_obj_set_style_pad_ver(_nav_bar, 3, 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_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); _nav_title = label(_nav_bar, "", THEME_FONT_BODY, theme::ACCENT);
lv_label_set_long_mode(_nav_title, LV_LABEL_LONG_DOT); 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 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]; char name[40];
bool person = false, set = false; bool person = false, set = false;
bool pos = navCurrentTarget(tlat, tlon, name, sizeof(name), person, set); bool pos = navCurrentTarget(tlat, tlon, name, sizeof(name), person, set);
if (!set) { // The bar only while there is a target: the map gets the whole screen
setText(_nav_title, "No target"); // otherwise (a target comes from the pin's list or a hold on the map).
setTextColor(_nav_title, theme::TEXT_MUTED); if (set == lv_obj_has_flag(_nav_bar, LV_OBJ_FLAG_HIDDEN)) {
setText(_nav_info, "Tap here to pick a target, or hold the map on a spot"); lv_obj_set_flag(_nav_bar, LV_OBJ_FLAG_HIDDEN, !set);
lv_obj_add_flag(_nav_clear, LV_OBJ_FLAG_HIDDEN); mapPlaceBottom();
return; if (navmap::s_ready) layoutMap(); // the names and markers clear of it, or not
} }
setTextColor(_nav_title, theme::ACCENT); if (!set) return;
lv_obj_remove_flag(_nav_clear, LV_OBJ_FLAG_HIDDEN);
setTextFmt(_nav_title, "%s %s", navmap::s_tb.active() ? LV_SYMBOL_LOOP : person ? UI_SYMBOL_USERS : UI_SYMBOL_FLAG, setTextFmt(_nav_title, "%s %s", navmap::s_tb.active() ? LV_SYMBOL_LOOP : person ? UI_SYMBOL_USERS : UI_SYMBOL_FLAG,
name); name);
if (!pos) { if (!pos) {
@@ -501,14 +501,14 @@ void UITask::navFrameTrail() {
} }
_map_follow = false; _map_follow = false;
int w = _map_area ? lv_obj_get_width(_map_area) : 320; 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; double dx = (x1 - x0) * mapview::TILE_PX, dy = (y1 - y0) * mapview::TILE_PX;
int z = mapview::MAX_Z - 1; 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 while (z > mapview::MIN_Z && (dx * (1 << z) > w - 110 || dy * (1 << z) > h - 60)) z--; // clear of the buttons
_map_z = z; _map_z = z;
double n = (double)(1 << z); double n = (double)(1 << z);
_map_cx = (x0 + x1) / 2 * n; _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(); layoutMap();
} }
@@ -591,6 +591,7 @@ void UITask::navClosePopup() {
if (_nav_overlay) lv_obj_delete_async(_nav_overlay); // may be closing from its own button 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_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; _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) { 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); 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. // 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) { void UITask::placeCard(lv_obj_t* parent, int32_t lat, int32_t lon) {
lv_obj_t* card = infoCard(parent); 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); 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* panel = navPopupPanel("Map tools", true);
lv_obj_t* list = scrollList(panel); lv_obj_t* list = scrollList(panel);
navmap::s_tools_list = list;
sectionTitle(list, "TRAIL"); sectionTitle(list, "TRAIL");
_nav_trail_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT); _nav_trail_lbl = label(list, "", THEME_FONT_SMALL, theme::TEXT);
@@ -1057,6 +990,10 @@ void UITask::navToolsPopup() {
#endif #endif
refreshNavTools(); 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() { 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_TRAIL: // one section of the Map options; back returns to the map
case navmap::TL_OPT_SHARE: case navmap::TL_OPT_SHARE:
case navmap::TL_OPT_ALERT: 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 showMapOptions(act == navmap::TL_OPT_TRAIL ? settings::SEC_TRAIL
: act == navmap::TL_OPT_SHARE ? settings::SEC_LIVE_SHARE : settings::SEC_LOCATOR); : act == navmap::TL_OPT_SHARE ? settings::SEC_LIVE_SHARE : settings::SEC_LOCATOR);
return; 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 // 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. // 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; const WaypointModel& wp = _core->waypoints;
char title[32]; lv_obj_t* panel = navPopupPanel("Places", true);
snprintf(title, sizeof(title), "Waypoints %d/%d", wp.count(), WaypointStore::CAPACITY);
lv_obj_t* panel = navPopupPanel(title, true);
lv_obj_t* r = buttonBar(panel); 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); toolButton(r, LV_SYMBOL_KEYBOARD " Coordinates", navmap::TL_WP_COORDS, false);
lv_obj_t* list = scrollList(panel); lv_obj_t* list = scrollList(panel);
int32_t lat = 0, lon = 0; int32_t lat = 0, lon = 0;
bool gps = _core->course.currentLocation(lat, lon); 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++) { for (int i = 0; i < wp.count(); i++) {
const Waypoint& w = wp.at(i); const Waypoint& w = wp.at(i);
char t[WAYPOINT_LABEL_LEN + 8], sub[40]; char title[WAYPOINT_LABEL_LEN + 8], sub[40], d[12];
snprintf(t, sizeof(t), UI_SYMBOL_FLAG " %s", w.label[0] ? w.label : "(unnamed)"); 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); 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); lv_obj_t* row = listRow(list, title, sub, onNavPick, (void*)(uintptr_t)navmap::code(navmap::T_WAYPOINT, i));
if (gps) { lv_obj_t* more = lv_button_create(row); // the waypoint menu
char d[12]; lv_obj_set_size(more, 40, 34);
geo::fmtDist(d, sizeof(d), geo::haversineKm(lat, lon, w.lat_1e6, w.lon_1e6), _prefs && _prefs->units_imperial); lv_obj_align(more, LV_ALIGN_RIGHT_MID, -4, 0);
navRowRight(row, d, theme::TEXT_MUTED, theme::PAD); 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) { 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 // "50.06142, 19.93721 Name" (decimal degrees; comma or space between them; the
// name is optional) -> a new waypoint. // name is optional) -> a new waypoint.
void UITask::navCoordsPopup() { 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, 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 }; 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 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) { (void)e; s_ui->toggleNearbySort(); } 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 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 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 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 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)); } 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) { 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_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_hor(b, 10, 0);
lv_obj_set_style_pad_ver(b, 0, 0); lv_obj_set_style_pad_ver(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), 0);
@@ -47,33 +52,18 @@ void UITask::showNearby() {
void UITask::buildNearby() { void UITask::buildNearby() {
lv_obj_t* body = newScreen("Nodes", true); lv_obj_t* body = newScreen("Nodes", true);
lv_obj_set_style_pad_row(body, 4, 0); lv_obj_set_style_pad_row(body, 4, 0);
if (_header) { if (_header) { // where they are, then who answers now (the advert: the quick panel)
headerButton(_header, LV_SYMBOL_REFRESH, onNearbyScan, 4, NULL); // scan headerButton(_header, UI_SYMBOL_MAP, onOpenNodesMap, 0, NULL);
lv_obj_set_width(headerButton(_header, "", onNearbySort, 46, &_nearby_sort_lbl), 62); // fits "Recent" / "Dist" headerButton(_header, LV_SYMBOL_REFRESH " Scan", onNearbyScan, 0, NULL);
headerButton(_header, UI_SYMBOL_MAP, onOpenNodesMap, 114, NULL); // the Nodes map (the advert: the quick panel)
} }
// Type filter chips // One line over the list: how many, then which and in what order.
_nearby_chips = lv_obj_create(body); _nearby_status = toolbarRow(body);
styleSurface(_nearby_chips, theme::BG); lv_obj_t* bar = lv_obj_get_parent(_nearby_status);
lv_obj_remove_flag(_nearby_chips, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_event_cb(toolbarChoice(bar, "All\nFavourites\nCompanions\nRepeaters\nRooms\nSensors", _nearby->filter(), "Show"),
lv_obj_set_size(_nearby_chips, LV_PCT(100), 26); onNearbyChip, LV_EVENT_VALUE_CHANGED, NULL);
lv_obj_set_flex_flow(_nearby_chips, LV_FLEX_FLOW_ROW); lv_obj_add_event_cb(toolbarChoice(bar, "Nearest\nLast heard", _nearby->sortMode() == NearbyModel::SORT_TIME ? 1 : 0, "Order"),
lv_obj_set_style_pad_column(_nearby_chips, 4, 0); onNearbySort, LV_EVENT_VALUE_CHANGED, NULL);
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);
_nearby_list = scrollList(body); _nearby_list = scrollList(body);
vlist::attach(_nearby_list); vlist::attach(_nearby_list);
@@ -87,9 +77,8 @@ void UITask::setNearbyFilter(uint8_t f) {
buildNearby(); buildNearby();
} }
void UITask::toggleNearbySort() { void UITask::setNearbySort(bool last_heard) {
_nearby->setSortMode(_nearby->sortMode() == NearbyModel::SORT_DIST ? NearbyModel::SORT_TIME _nearby->setSortMode(last_heard ? NearbyModel::SORT_TIME : NearbyModel::SORT_DIST);
: NearbyModel::SORT_DIST);
_nearby_sig = 0; _nearby_sig = 0;
refreshNearbyList(); refreshNearbyList();
} }
@@ -244,12 +233,9 @@ void UITask::refreshNearbyList() {
uint32_t sig = nearbySignature(); uint32_t sig = nearbySignature();
int n = _nearby->count(); int n = _nearby->count();
if (_nearby_sort_lbl)
setText(_nearby_sort_lbl, _nearby->sortMode() == NearbyModel::SORT_TIME ? "Recent" : "Dist");
int32_t lat, lon; int32_t lat, lon;
bool gps = _nearby->ownPosition(lat, lon); bool gps = _nearby->ownPosition(lat, lon);
setTextFmt(_nearby_status, "%d node%s%s", n, n == 1 ? "" : "s", setTextFmt(_nearby_status, "%d node%s%s", n, n == 1 ? "" : "s", gps ? "" : ", no GPS");
gps ? "" : " - no GPS fix, distances unknown");
if (sig == _nearby_sig || vlist::pressed(_nearby_list)) return; // held: once it's let go if (sig == _nearby_sig || vlist::pressed(_nearby_list)) return; // held: once it's let go
_nearby_sig = sig; _nearby_sig = sig;
@@ -324,9 +310,25 @@ void UITask::buildNode() {
const NearbyModel::Entry& e = s_node; const NearbyModel::Entry& e = s_node;
lv_obj_t* body = newScreen(e.name[0] ? e.name : "(unknown)", true); 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; s_nd_fav = nullptr;
_node_path_lbl = 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); nodeVizBuild(info);
nodeviz::s_bearing = nodeviz::s_hops = -2; // nothing drawn yet: the first set fills both nodeviz::s_bearing = nodeviz::s_hops = -2; // nothing drawn yet: the first set fills both
_node_info = infoCard(info); _node_info = infoCard(info);
@@ -337,19 +339,6 @@ void UITask::buildNode() {
s_nd_heard = infoRow(_node_info, "Heard", ""); s_nd_heard = infoRow(_node_info, "Heard", "");
s_nd_signal = infoRow(_node_info, "Signal", ""); s_nd_signal = infoRow(_node_info, "Signal", "");
s_nd_id = infoRow(_node_info, "ID", ""); 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) { if (pos || contact) {
lv_obj_t* g = group(info, "More"); lv_obj_t* g = group(info, "More");
@@ -449,10 +438,12 @@ void UITask::refreshNode() {
if (!_core->ping.isActive() && (out || back || rtt)) { if (!_core->ping.isActive() && (out || back || rtt)) {
setTextFmt(_node_ping, "Ping %lu ms - SNR out %.1f back %.1f", (unsigned long)rtt, setTextFmt(_node_ping, "Ping %lu ms - SNR out %.1f back %.1f", (unsigned long)rtt,
out / 4.0f, back / 4.0f); out / 4.0f, back / 4.0f);
lv_obj_remove_flag(_node_ping, LV_OBJ_FLAG_HIDDEN);
_pinging = false; _pinging = false;
} else if (millis() - _ping_started_ms > _core->ping.timeoutMs()) { } else if (millis() - _ping_started_ms > _core->ping.timeoutMs()) {
_core->ping.clear(); _core->ping.clear();
setText(_node_ping, "Ping: no reply"); setText(_node_ping, "Ping: no reply");
lv_obj_remove_flag(_node_ping, LV_OBJ_FLAG_HIDDEN);
_pinging = false; _pinging = false;
} }
} }
@@ -467,6 +458,7 @@ void UITask::nodeAction(uint8_t action) {
case NODE_PING: { case NODE_PING: {
if (_pinging) break; if (_pinging) break;
PingEngine::StartResult r = _core->ping.start(e.pub_key); PingEngine::StartResult r = _core->ping.start(e.pub_key);
lv_obj_remove_flag(_node_ping, LV_OBJ_FLAG_HIDDEN);
if (r == PingEngine::STARTED) { if (r == PingEngine::STARTED) {
_pinging = true; _pinging = true;
_ping_started_ms = millis(); _ping_started_ms = millis();
@@ -43,6 +43,8 @@ static void hidden(lv_anim_t* a) {
(void)a; (void)a;
lv_obj_add_flag(s_panel, LV_OBJ_FLAG_HIDDEN); 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 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) { static void slide(int32_t to, bool away) {
lv_anim_delete(s_panel, NULL); lv_anim_delete(s_panel, NULL);
@@ -134,7 +136,7 @@ static void onStatusBarTouch(lv_event_t* e) {
s_drag = false; s_drag = false;
if (dy > s_h / 3) { s_open = true; slide(0, false); } if (dy > s_h / 3) { s_open = true; slide(0, false); }
else s_ui->quickPanelClose(true); 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); if (s_open) s_ui->quickPanelClose(true);
else if (s_ui->quickPanelBegin()) { s_open = true; slide(0, false); } 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 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); 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 lv_obj_scroll_to_x(s_pages, 0, LV_ANIM_OFF); // the first page each time
dotsSet(0); dotsSet(0);
layoutNow(s_panel); 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_size(b, LV_SIZE_CONTENT, 28);
lv_obj_set_style_pad_hor(b, 8, 0); lv_obj_set_style_pad_hor(b, 8, 0);
lv_obj_set_style_pad_ver(b, 0, 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_shadow_width(b, 0, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 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); 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_t* b = lv_button_create(row);
lv_obj_set_size(b, 30, 26); lv_obj_set_size(b, 30, 26);
lv_obj_set_style_pad_all(b, 0, 0); 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_shadow_width(b, 0, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 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); 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); styleSurface(r, theme::SURFACE);
lv_obj_remove_flag(r, LV_OBJ_FLAG_SCROLLABLE); lv_obj_remove_flag(r, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(r, LV_PCT(100), h); 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_hor(r, 8, 0);
lv_obj_set_style_pad_ver(r, 0, 0); lv_obj_set_style_pad_ver(r, 0, 0);
lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW); 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_t* c = lv_button_create(s_strip);
lv_obj_set_size(c, 38, 44); lv_obj_set_size(c, 38, 44);
lv_obj_set_style_pad_all(c, 0, 0); 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_shadow_width(c, 0, 0);
lv_obj_set_style_border_color(c, lv_color_hex(theme::ACCENT), 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); 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_t* b = lv_button_create(parent);
lv_obj_set_size(b, 40, 32); lv_obj_set_size(b, 40, 32);
lv_obj_set_style_pad_all(b, 0, 0); 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_shadow_width(b, 0, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 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); 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) // Spacing (px)
static const int PAD = 8; static const int PAD = 8;
static const int GAP = 6; static const int GAP = 6;
static const int RADIUS = 10; // cards, rows, buttons (pills use LV_RADIUS_CIRCLE) static const int RADIUS = 10; // cards, rows and everything tapped: buttons, tabs, choices, chips
static const int RADIUS_SM = 6; // small cells inside those static const int RADIUS_SM = 6; // small drawn cells inside those (chart backgrounds, captions)
static const int STATUS_H = 22; // top status bar 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) 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). // Type -- Noto Sans, generated by fonts/generate.sh (declared in lv_conf.h).
// ui_font_40 holds digits / ':' / '-' / '.' only (clock, timer, stopwatch). // ui_font_40 holds digits / ':' / '-' / '.' only (clock, timer, stopwatch).
@@ -64,9 +64,10 @@ void UITask::setKeyboardVisible(bool show) {
fieldFocus(_compose_ta, false); fieldFocus(_compose_ta, false);
} }
if (_header && _body) { 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); if (show) lv_obj_add_flag(_header, LV_OBJ_FLAG_HIDDEN);
else lv_obj_remove_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_pos(_body, 0, top);
lv_obj_set_height(_body, lv_display_get_vertical_resolution(NULL) - 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_all(body, 0, 0);
lv_obj_set_style_pad_row(body, 0, 0); lv_obj_set_style_pad_row(body, 0, 0);
if (_header && (_thread_is_channel || can_send)) { // channel / conversation options 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); 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_size(b, LV_SIZE_CONTENT, 28);
lv_obj_set_style_pad_hor(b, 10, 0); lv_obj_set_style_pad_hor(b, 10, 0);
lv_obj_set_style_pad_ver(b, 0, 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_shadow_width(b, 0, 0);
// on our own amber bubble the main one goes dark instead // 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); 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_anim_delete(s_banner, NULL);
lv_obj_remove_flag(s_banner, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(s_banner, LV_OBJ_FLAG_HIDDEN);
if (!shown) { // a new one while it's up just changes the text 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); layoutNow(s_banner);
anim::run(s_banner, anim::setTy, -(lv_obj_get_height(s_banner) + theme::STATUS_H), 0, anim::SCREEN_MS); anim::run(s_banner, anim::setTy, -(lv_obj_get_height(s_banner) + theme::STATUS_H), 0, anim::SCREEN_MS);
} else { } else {
@@ -715,8 +716,20 @@ void UITask::refreshStatusBar() {
default: break; default: break;
} }
setTextFmt(_status_batt, "%s%s", batt, level); 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 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); lv_obj_remove_flag(_status_chg, LV_OBJ_FLAG_HIDDEN);
placeLeftOf(_status_chg, _status_batt, -2); placeLeftOf(_status_chg, _status_batt, -2);
left_of = _status_chg; left_of = _status_chg;
@@ -760,6 +773,16 @@ void UITask::refreshStatusBar() {
placeLeftOf(_status_icons, left_of, -5); 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) { void UITask::setGps(bool on) {
if (!_core->setGpsEnabled(on, false)) { showToast("No GPS on this device"); return; } if (!_core->setGpsEnabled(on, false)) { showToast("No GPS on this device"); return; }
prefsSave(); // written on leaving the screen, as the other switches 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); bool shown = !lv_obj_has_flag(_toast, LV_OBJ_FLAG_HIDDEN);
lv_anim_delete(_toast, NULL); // on its way out lv_anim_delete(_toast, NULL); // on its way out
lv_obj_remove_flag(_toast, LV_OBJ_FLAG_HIDDEN); lv_obj_remove_flag(_toast, LV_OBJ_FLAG_HIDDEN);
// Under the message banner while that's up, else right under the status bar. // Under the message banner while that's up, else right under the top bar.
int32_t y = theme::STATUS_H + 4; 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; 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); lv_obj_align(_toast, LV_ALIGN_TOP_MID, 0, y);
layoutNow(_toast); 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); if (_shown_screen == SCR_SETTINGS && _body) s_settings_y = lv_obj_get_scroll_y(_body);
pickerClose(); pickerClose();
_header = _body = nullptr; _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; _node_info = _node_ping = _node_delete_lbl = nullptr;
nodeviz::s_card = nullptr; nodeviz::s_card = nullptr;
_scan_overlay = _scan_list = _scan_status = nullptr; // the popup went with the old screen _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); styleOpaque(scr, theme::BG);
lv_obj_remove_flag(scr, LV_OBJ_FLAG_SCROLLABLE); 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; int top = theme::STATUS_H;
_hdr_clock = _hdr_time = _hdr_batt = nullptr;
if (title) { if (title) {
lv_obj_t* hdr = lv_obj_create(scr); lv_obj_t* hdr = lv_obj_create(scr);
styleSurface(hdr, theme::BG); styleSurface(hdr, theme::BG);
lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE); lv_obj_remove_flag(hdr, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(hdr, LV_PCT(100), 38); lv_obj_set_size(hdr, LV_PCT(100), theme::APPBAR_H);
lv_obj_set_pos(hdr, 0, top); 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; _header = hdr;
if (with_back) { if (with_back) {
lv_obj_t* b = lv_button_create(hdr); lv_obj_t* b = lv_button_create(hdr);
lv_obj_set_size(b, 40, 28); lv_obj_set_size(b, 40, theme::BAR_BTN_H);
lv_obj_align(b, LV_ALIGN_LEFT_MID, 4, 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), 0);
lv_obj_set_style_shadow_width(b, 0, 0); lv_obj_set_style_shadow_width(b, 0, 0);
lv_obj_add_event_cb(b, onBack, LV_EVENT_CLICKED, NULL); 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_obj_t* t = label(hdr, title, THEME_FONT_TITLE, theme::TEXT);
lv_label_set_long_mode(t, LV_LABEL_LONG_DOT); lv_label_set_long_mode(t, LV_LABEL_LONG_DOT);
lv_obj_set_width(t, 230); lv_obj_set_width(t, 1);
lv_obj_align(t, LV_ALIGN_LEFT_MID, with_back ? 52 : theme::PAD, 0); lv_obj_set_flex_grow(t, 1); // what the actions leave
top += 38; // 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); 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); if (prev && !same && !_asleep) anim::screenIn(body, backward);
} }
_fade_next = false; _fade_next = false;
statusBarSync();
refreshStatusBar(); // the title bar's time and battery
return body; return body;
} }
@@ -1103,8 +1169,12 @@ void UITask::back() {
case SCR_SETTINGS: if (_nav_overlay) navClosePopup(); else showHome(); break; case SCR_SETTINGS: if (_nav_overlay) navClosePopup(); else showHome(); break;
case SCR_SETTINGS_NAV: // the map's options go back to the map case SCR_SETTINGS_NAV: // the map's options go back to the map
if (_settings_page == settings::PG_NAV && s_opts_from_map) { if (_settings_page == settings::PG_NAV && s_opts_from_map) {
openMap(true); // The map as it was left -- its mode and view, Map tools over it,
if (s_nav_section >= 0) navToolsPopup(); // one section's options: back into Map tools // 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) { } else if (_settings_page == settings::PG_RADIO) {
showRadio(); showRadio();
} else { } else {
@@ -1145,7 +1215,7 @@ void UITask::back() {
else showHome(); else showHome();
break; break;
case SCR_NODE: // back to where the node was picked: map, the list, or the scan popup over it 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; _screen = SCR_NEARBY;
buildNearby(); buildNearby();
if (_node_from_scan) showScanPopup(); if (_node_from_scan) showScanPopup();
@@ -1355,3 +1425,36 @@ extern "C" EMSCRIPTEN_KEEPALIVE int sim_scroll(int dy) {
return left; return left;
} }
#endif #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 setSchemaValue(int idx, int v);
void setMsgBanner(bool on); // Settings > Display > Message banner void setMsgBanner(bool on); // Settings > Display > Message banner
void showNearby(); 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 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 mapLongPress(int x, int y);
void mapZoom(int delta); void mapZoom(int delta);
@@ -178,7 +178,7 @@ public:
void navMarkHere(); void navMarkHere();
void navAveragingCancel(); void navAveragingCancel();
void navToNode(const uint8_t* key, int32_t lat, int32_t lon, const char* name); 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 navToolAction(uint8_t act);
void navSetShareTarget(int sel); void navSetShareTarget(int sel);
void navWaypointsPopup(); void navWaypointsPopup();
@@ -197,7 +197,7 @@ public:
void wifiEdit(lv_obj_t* ta); void wifiEdit(lv_obj_t* ta);
void wifiKeyboardHide(); void wifiKeyboardHide();
void setNearbyFilter(uint8_t f); void setNearbyFilter(uint8_t f);
void toggleNearbySort(); void setNearbySort(bool last_heard);
void startNearbyScan(); void startNearbyScan();
void closeScanPopup(); void closeScanPopup();
void openScanNode(int row); void openScanNode(int row);
@@ -368,6 +368,7 @@ public:
void openRouteFor(const uint8_t* pub_key) { showRoute(pub_key); } void openRouteFor(const uint8_t* pub_key) { showRoute(pub_key); }
void openMessagesList() { showChats(); } // sim harness void openMessagesList() { showChats(); } // sim harness
bool openNodesMap() { openMap(false); return true; } // sim harness bool openNodesMap() { openMap(false); return true; } // sim harness
void statusBarSync(); // up or hidden, by the screen's title bar (UITask.cpp)
private: 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 }; 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 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; void mapPointToLatLon(int x, int y, int32_t& lat_e6, int32_t& lon_e6) const;
double mapCenterBias() const; double mapCenterBias() const;
int mapBottomH() const;
void mapPlaceBottom();
void buildNavLayers(); void buildNavLayers();
void buildNavControls(lv_obj_t* body); void buildNavControls(lv_obj_t* body);
void rebuildNavMarkers(); void rebuildNavMarkers();
@@ -605,6 +608,9 @@ private:
lv_obj_t* _home_date = nullptr; lv_obj_t* _home_date = nullptr;
lv_obj_t* _home_unread = nullptr; lv_obj_t* _home_unread = nullptr;
lv_obj_t* _header = nullptr; // current screen's title bar (nullptr on Home) 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* _body = nullptr;
lv_obj_t* _thread_list = nullptr; lv_obj_t* _thread_list = nullptr;
lv_obj_t* _compose_ta = 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 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_list = nullptr;
lv_obj_t* _nearby_status = 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_info = nullptr;
lv_obj_t* _node_ping = nullptr; lv_obj_t* _node_ping = nullptr;
lv_obj_t* _node_delete_lbl = 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() { 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; 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 // 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 // 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 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); 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); s_strip = lv_obj_create(overlay);
lv_obj_remove_style_all(s_strip); lv_obj_remove_style_all(s_strip);
lv_obj_remove_flag(s_strip, LV_OBJ_FLAG_CLICKABLE); 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 // 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). // 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 }; 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) { static lv_obj_t* popupOpen(lv_obj_t* parent, PopFit fit, lv_obj_t*& overlay, int32_t inset = 8) {
overlay = dimOverlay(parent); overlay = dimOverlay(parent);
s_fit_pending = true; // rowsFit() 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_all(panel, theme::PAD, 0);
lv_obj_set_style_pad_row(panel, 4, 0); lv_obj_set_style_pad_row(panel, 4, 0);
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN); 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 if (fit == POP_BOTTOM) { // over a live screen, nothing dimmed
lv_obj_set_style_bg_opa(overlay, LV_OPA_TRANSP, 0); lv_obj_set_style_bg_opa(overlay, LV_OPA_TRANSP, 0);
anim::rise(panel); anim::rise(panel);
} else { } else {
lv_obj_add_event_cb(overlay, freeze::ownerDeleted, LV_EVENT_DELETE, NULL); 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); else anim::popup(overlay, LV_OPA_60, freeze::onDimUp);
} }
return panel; 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_size(b, LV_SIZE_CONTENT, 24);
lv_obj_set_style_pad_hor(b, 10, 0); lv_obj_set_style_pad_hor(b, 10, 0);
lv_obj_set_style_pad_ver(b, 0, 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_shadow_width(b, 0, 0);
lv_obj_set_style_bg_color(b, lv_color_hex(bg), 0); lv_obj_set_style_bg_color(b, lv_color_hex(bg), 0);
lv_obj_align(b, LV_ALIGN_RIGHT_MID, -2, 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 ...). // at it (the screen titles, a list row's cut-off name ...).
static void rowsFit(lv_obj_t* root) { static void rowsFit(lv_obj_t* root) {
lv_obj_tree_walk(root, [](lv_obj_t* o, void*) { lv_obj_tree_walk(root, [](lv_obj_t* o, void*) {
if (lv_obj_has_flag(o, LV_OBJ_FLAG_USER_2)) { if (lv_obj_has_flag(o, LV_OBJ_FLAG_USER_2)) {
lv_obj_remove_flag(o, LV_OBJ_FLAG_USER_2); lv_obj_remove_flag(o, LV_OBJ_FLAG_USER_2);
rowFit(o); 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); 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); 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))) { 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_color(c, lv_color_hex(theme::SURFACE_2), 0);
lv_obj_set_style_bg_opa(c, LV_OPA_COVER, 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_hor(c, 10, 0);
lv_obj_set_style_pad_ver(c, 8, 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; 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. // 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, 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_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_text_font(m, THEME_FONT_SMALL, LV_PART_ITEMS);
lv_obj_set_style_shadow_width(m, 0, 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; return m;
} }