feat(l1): one title bar, page dots, disclosure marks and push buttons

drawScreenHeader replaces drawCenteredHeader on every ui-new screen: the
title in normal case, page dots instead of "TRAIL 1/3", the menu hint, no
icon (the screen was just picked by one). Accordions in Settings and Tools
show a disclosure triangle instead of +/-, Tools sections drop their icons.
Form "[Save]" rows and "+ Send" share drawButton; form fields use valueRow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-02 08:45:05 +02:00
co-authored by Claude Opus 5.5
parent 4ba171d6d8
commit 6d8e56fbd0
18 changed files with 159 additions and 104 deletions
@@ -178,7 +178,7 @@ public:
if (_login_kb) return kb().render(display);
AdminSession::State st = S().state();
if (st == AdminSession::LOGGING_IN || st == AdminSession::IDLE || st == AdminSession::NEED_PASSWORD) {
display.drawCenteredHeader("ADMIN LOGIN");
drawScreenHeader(display, "Admin login");
if (st != AdminSession::LOGGING_IN) return 500;
return waiting(display, "Logging in");
}
@@ -187,7 +187,7 @@ public:
if (st == AdminSession::WAITING) {
char title[24];
snprintf(title, sizeof(title), "%.23s", S().target().name);
display.drawCenteredHeader(title);
drawScreenHeader(display, title);
return waiting(display, S().fetching() ? "Fetching" : "Waiting for reply");
}
@@ -28,7 +28,7 @@ public:
int render(DisplayDriver& display) override {
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
display.drawCenteredHeader("AUTO-ADVERT");
drawScreenHeader(display, "Auto-advert");
int sel = 0, scroll = 0;
drawList(display, 1, sel, scroll, [&](int, int y, bool is_sel, int reserve) {
drawRowSelection(display, y, is_sel, reserve);
+15 -18
View File
@@ -30,6 +30,7 @@
// existing CMD_GET_CHANNEL comment ("NOTE: only 128-bit supported").
#include "../ui-core/ChannelControl.h"
#include "InfoKit.h"
class ChannelsView {
UITask* _task;
@@ -45,7 +46,7 @@ class ChannelsView {
char _secret_text[33] = ""; // passphrase or hex string typed so far
char _topic[31] = ""; // ADD_HASHTAG: topic without the leading '#'
int _sel = 0; // 0=Name 1=Secret 2=[Save] (ADD_HASHTAG: 0=Topic 1=[Save])
int _sel = 0; // 0=Name 1=Secret 2=Save (ADD_HASHTAG: 0=Topic 1=Save)
bool _kb_active = false;
int _kb_field = -1; // 0=Name, 1=Secret, 2=Topic
@@ -117,7 +118,7 @@ public:
const int step = display.lineStep();
if (_mode == TYPE_PICK) {
display.drawCenteredHeader("ADD CHANNEL");
drawScreenHeader(display, "Add channel");
static const char* TYPE_LABELS[3] = { "Public", "Hashtag", "Private" };
for (int i = 0; i < 3; i++) {
int y = top + i * step;
@@ -129,37 +130,33 @@ public:
}
if (_mode == ADD_HASHTAG) {
display.drawCenteredHeader("ADD CHANNEL");
drawScreenHeader(display, "Add channel");
int mq_delay = 0;
for (int i = 0; i < 2; i++) {
int y = top + i * step;
bool sel = (i == _sel);
if (i == 1) { drawButton(display, (display.width() - buttonWidth(display, "Save")) / 2, y, "Save", sel); continue; }
display.drawSelectionRow(0, y - 1, display.width(), step - 1, sel);
char row[40];
if (i == 0) snprintf(row, sizeof(row), "Topic: %s", _topic[0] ? _topic : "(none)");
else snprintf(row, sizeof(row), "[Save]");
int r = display.drawTextEllipsized(2, y, display.width() - 4, row, sel);
if (sel && r > 0) mq_delay = r;
int r = info::valueRow(display, y, "Topic", _topic[0] ? _topic : "(none)", sel, 0);
if (r > 0) mq_delay = r;
display.setColor(DisplayDriver::LIGHT);
}
return (mq_delay > 0 && mq_delay < 1000) ? mq_delay : 1000;
}
display.drawCenteredHeader(_mode == ADD ? "ADD CHANNEL" : "EDIT CHANNEL");
drawScreenHeader(display, _mode == ADD ? "Add channel" : "Edit channel");
int mq_delay = 0;
for (int i = 0; i < 3; i++) {
int y = top + i * step;
bool sel = (i == _sel);
if (i == 2) { drawButton(display, (display.width() - buttonWidth(display, "Save")) / 2, y, "Save", sel); continue; }
display.drawSelectionRow(0, y - 1, display.width(), step - 1, sel);
char row[40];
if (i == 0) snprintf(row, sizeof(row), "Name: %s", _name[0] ? _name : "(none)");
else if (i == 1) snprintf(row, sizeof(row), "Secret (%s): %s",
_hex_mode ? "hex" : "phrase", _secret_text[0] ? _secret_text : "(none)");
else snprintf(row, sizeof(row), "[Save]");
// Ellipsize rather than print() directly -- a long name/secret must not
// wrap onto the next row's line (print() wraps by default).
int r = display.drawTextEllipsized(2, y, display.width() - 4, row, sel);
if (sel && r > 0) mq_delay = r;
// valueRow cuts a long name/secret (scrolling it while selected) rather
// than letting print() wrap it onto the next row's line.
int r = i == 0 ? info::valueRow(display, y, "Name", _name[0] ? _name : "(none)", sel, 0)
: info::valueRow(display, y, _hex_mode ? "Secret (hex)" : "Secret (phrase)",
_secret_text[0] ? _secret_text : "(none)", sel, 0);
if (r > 0) mq_delay = r;
display.setColor(DisplayDriver::LIGHT);
}
return (mq_delay > 0 && mq_delay < 1000) ? mq_delay : 1000;
@@ -146,7 +146,7 @@ class ClockToolsScreen : public UIScreen {
// ── Sub-renders ───────────────────────────────────────────────────────────
int renderMenu(DisplayDriver& d) {
d.drawCenteredHeader("CLOCK TOOLS");
drawScreenHeader(d, "Clock tools");
static const char* items[3] = { "Alarm", "Timer", "Stopwatch" };
if (_sel > 2) _sel = 2;
drawList(d, 3, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
@@ -163,7 +163,7 @@ class ClockToolsScreen : public UIScreen {
}
int renderAlarm(DisplayDriver& d) {
d.drawCenteredHeader("ALARM");
drawScreenHeader(d, "Alarm");
if (!_prefs) return 60000;
const char* rows[3] = { "Time", "Repeat", "Armed" };
if (_sel > 2) _sel = 2;
@@ -186,7 +186,7 @@ class ClockToolsScreen : public UIScreen {
}
int renderTimer(DisplayDriver& d) {
d.drawCenteredHeader("TIMER");
drawScreenHeader(d, "Timer");
const int cx = d.width() / 2;
if (_task->isTimerRunning()) {
char buf[16];
@@ -215,7 +215,7 @@ class ClockToolsScreen : public UIScreen {
}
int renderStopwatch(DisplayDriver& d) {
d.drawCenteredHeader("STOPWATCH");
drawScreenHeader(d, "Stopwatch");
const int cx = d.width() / 2;
char buf[16];
bool tenths = !d.isEink() && _sw_running; // tenths only on a fast panel
@@ -32,7 +32,7 @@ public:
int render(DisplayDriver& display) override {
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
display.drawCenteredHeader("COMPASS");
drawScreenHeader(display, "Compass");
const int cx = display.width() / 2;
const int ch = display.getLineHeight();
@@ -31,7 +31,7 @@ public:
int start_y = display.listStart();
int val_x = display.valCol();
display.drawCenteredHeader("CLOCK FIELDS");
drawScreenHeader(display, "Clock fields");
static const char* labels[] = { "Field 1", "Field 2", "Field 3" };
for (int i = 0; i < FIELD_SLOTS; i++) {
+1 -1
View File
@@ -111,7 +111,7 @@ public:
buildItems();
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
display.drawCenteredHeader("GPIO");
drawScreenHeader(display, "GPIO");
bool any_live = false;
drawList(display, _item_count, _sel, _scroll, [&](int row, int y, bool sel, int reserve) {
@@ -684,7 +684,7 @@ struct KeyboardWidget {
display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, chars_y, display.width(), hh);
display.setColor(DisplayDriver::DARK);
display.drawTextCentered(display.width() / 2, chars_y + 1, "CURSOR MODE");
display.drawTextCentered(display.width() / 2, chars_y + 1, "Cursor mode");
display.setColor(DisplayDriver::LIGHT);
display.drawTextCentered(display.width() / 2, chars_y + hh + 2, "L/R move");
display.drawTextCentered(display.width() / 2, chars_y + hh + 2 + lh, "U/D start/end");
@@ -101,7 +101,7 @@ public:
int render(DisplayDriver& display) override {
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
display.drawCenteredHeader("LIVE SHARE");
drawScreenHeader(display, "Live share");
int mq_delay = 0;
drawList(display, ROW_COUNT, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
@@ -110,7 +110,7 @@ public:
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
if (_picking) { int mq = renderPicker(display); return (mq > 0 && mq < 400) ? mq : 400; }
display.drawCenteredHeader("LOCATOR");
drawScreenHeader(display, "Locator");
const int rc = visibleRows();
if (_sel >= rc) _sel = rc - 1; // beeper row may have just been hidden
@@ -261,7 +261,7 @@ public:
// Returns 0, or the ms until the selected row should next redraw to keep a
// marquee animation going (see DisplayDriver::drawTextEllipsized).
int renderPicker(DisplayDriver& display) {
display.drawCenteredHeader("PICK TARGET");
drawScreenHeader(display, "Pick target");
uint32_t now = rtc_clock.getCurrentTime();
int mq_delay = 0;
drawList(display, _target_n, _pick_sel, _pick_scroll, [&](int i, int y, bool sel, int reserve) {
@@ -565,16 +565,9 @@ class MessagesScreen : public UIScreen {
// reset to LIGHT first so it stays visible regardless of the ink the
// message loop left.
static void drawComposeButton(DisplayDriver& d, int cby, int lh, bool sel) {
(void)lh;
const char* ctxt = "+ Send";
int ctw = d.getTextWidth(ctxt);
int bw = ctw + 6;
int bx = d.width() - bw;
d.setColor(DisplayDriver::LIGHT);
if (sel) { d.fillSoftRect(bx, cby, bw, lh); d.setColor(DisplayDriver::DARK); }
else d.drawSoftRect(bx, cby, bw, lh);
d.setCursor(bx + 3, cby);
d.print(ctxt);
d.setColor(DisplayDriver::LIGHT);
drawButton(d, d.width() - buttonWidth(d, ctxt), cby, ctxt, sel);
}
void afterSend(bool ok, const char* msg) {
@@ -1206,11 +1199,11 @@ public:
if (_phase == MODE_SELECT) {
// Say which dial slot is being filled -- entered from the Favourites page,
// a bare "MESSAGE" gives no sign that this browse is a pick.
// a bare "Messages" gives no sign that this browse is a pick.
char hdr[16];
if (_pick_fav_slot >= 0) snprintf(hdr, sizeof(hdr), "PIN SLOT %d", _pick_fav_slot + 1);
else snprintf(hdr, sizeof(hdr), "MESSAGE");
display.drawCenteredHeader(hdr, true, _ctx_menu.active);
if (_pick_fav_slot >= 0) snprintf(hdr, sizeof(hdr), "Pin slot %d", _pick_fav_slot + 1);
else snprintf(hdr, sizeof(hdr), "Messages");
drawScreenHeader(display, hdr, -1, 0, true, _ctx_menu.active);
const char* opts[] = { "Direct message", "Channels", "Room Servers" };
int badges[3] = {
getDMUnreadTotal(),
@@ -1236,7 +1229,7 @@ public:
if (_ctx_menu.active) { int r = _ctx_menu.render(display); if (mq_delay <= 0 || r < mq_delay) mq_delay = r; }
} else if (_phase == CONTACT_PICK) {
display.drawCenteredHeader(_room_mode ? "SELECT ROOM" : "SELECT CONTACT", true, _ctx_menu.active);
drawScreenHeader(display, _room_mode ? "Rooms" : "Contacts", -1, 0, true, _ctx_menu.active);
if (_num_contacts == 0) {
display.drawTextCentered(display.width()/2, display.height()/2, _room_mode ? "No room servers" : "No favourites");
@@ -1276,7 +1269,7 @@ public:
if (_ctx_menu.active) { int r = _ctx_menu.render(display); if (mq_delay <= 0 || r < mq_delay) mq_delay = r; }
} else if (_phase == CHANNEL_PICK) {
display.drawCenteredHeader("SELECT CHANNEL", true, _ctx_menu.active);
drawScreenHeader(display, "Channels", -1, 0, true, _ctx_menu.active);
// "+ Add channel" is a synthetic trailing row — suppressed while picking
// a channel for the bot, so that picker's list stays unchanged.
@@ -1360,7 +1353,7 @@ public:
char title[24];
snprintf(title, sizeof(title), "%.23s", filtered_name);
display.drawCenteredHeader(title, true, _ctx_menu.active);
drawScreenHeader(display, title, -1, 0, true, _ctx_menu.active);
int dm_count = _history.dmHistCountForContact(_sel_contact.id.pub_key);
uint32_t now_ts = rtc_clock.getCurrentTime();
@@ -1660,7 +1653,7 @@ public:
} else {
snprintf(title, sizeof(title), "%.23s", ch.name);
}
display.drawCenteredHeader(title, true, _ctx_menu.active);
drawScreenHeader(display, title, -1, 0, true, _ctx_menu.active);
int ch_hist_count = _history.histCountForChannel(_sel_channel_idx);
uint32_t now_ts = rtc_clock.getCurrentTime();
@@ -1840,15 +1833,15 @@ public:
char nick_raw[32], nick_trans[32];
snprintf(nick_raw, sizeof(nick_raw), "%.*s", rlen, _reply_prefix + 2);
display.translateUTF8ToBlocks(nick_trans, nick_raw, sizeof(nick_trans));
snprintf(title, sizeof(title), "RE:%s", nick_trans);
snprintf(title, sizeof(title), "Re: %s", nick_trans);
} else if (_sending_to_channel) {
ChannelDetails ch;
the_mesh.getChannel(_sel_channel_idx, ch);
snprintf(title, sizeof(title), "%.23s", ch.name);
} else {
snprintf(title, sizeof(title), "TO:%.14s", _sel_contact.name);
snprintf(title, sizeof(title), "To: %.14s", _sel_contact.name);
}
display.drawCenteredHeader(title);
drawScreenHeader(display, title);
int total_msg_items = 1 + _active_msg_count;
drawList(display, total_msg_items, _msg_sel, _msg_scroll, [&](int idx, int y, bool sel, int reserve) {
@@ -616,14 +616,14 @@ public:
const char* flt = (_filter != F_ALL) ? filterLabel(_filter) : nullptr;
if (_source == SRC_SCAN) {
char title[28];
const char* base = _scanning ? "SCANNING" : "SCAN";
const char* base = _scanning ? "Scanning" : "Scan";
if (flt) {
if (!_scanning && _count == 0) snprintf(title, sizeof(title), "SCAN %s: none", flt);
if (!_scanning && _count == 0) snprintf(title, sizeof(title), "Scan %s: none", flt);
else snprintf(title, sizeof(title), "%s %s (%d)", base, flt, _count);
} else if (_scanning) snprintf(title, sizeof(title), "SCANNING (%d)", _count);
else if (_count == 0) snprintf(title, sizeof(title), "SCAN: none");
else snprintf(title, sizeof(title), "SCAN (%d)", _count);
display.drawCenteredHeader(title, true, ctxMenuOpen());
} else if (_scanning) snprintf(title, sizeof(title), "Scanning (%d)", _count);
else if (_count == 0) snprintf(title, sizeof(title), "Scan: none");
else snprintf(title, sizeof(title), "Scan (%d)", _count);
drawScreenHeader(display, title, -1, 0, true, ctxMenuOpen());
} else {
// Stored list: the filter is a visible tab strip (LEFT/RIGHT switches tabs).
drawFilterTabs(display);
@@ -633,7 +633,7 @@ public:
char empty[24];
if (_source == SRC_SCAN) {
const char* msg;
if (_scanning) { // the header already says SCANNING: just show it's listening
if (_scanning) { // the header already says Scanning: just show it's listening
display.drawTextCentered(display.width() / 2, display.height() / 2 - display.lineStep() / 2, "No replies yet");
int r = drawLoadingDots(display, display.width() / 2, display.height() / 2 + display.lineStep() + 2);
if (mq_delay <= 0 || r < mq_delay) mq_delay = r;
@@ -645,8 +645,8 @@ public:
} else {
const char* hint;
if (_pick_admin_target && !flt) { snprintf(empty, sizeof(empty), "Nothing to admin"); hint = "Repeaters & rooms"; }
else if (flt) { snprintf(empty, sizeof(empty), "No %s contacts", flt); hint = "[<>] change filter"; }
else { snprintf(empty, sizeof(empty), "No contacts found"); hint = "[Enter]=Discover"; }
else if (flt) { snprintf(empty, sizeof(empty), "No %s contacts", flt); hint = "Left/Right: filter"; }
else { snprintf(empty, sizeof(empty), "No contacts found"); hint = "Enter: discover";}
display.drawTextCentered(display.width() / 2, display.height() / 2 - display.lineStep() / 2, empty);
display.drawTextCentered(display.width() / 2, display.height() / 2 + display.lineStep() / 2, hint);
}
@@ -150,7 +150,7 @@ public:
buildItems(p);
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
display.drawCenteredHeader("REPEATER");
drawScreenHeader(display, "Repeater");
// Config only — live forwarding stats live on Status › Mesh.
drawList(display, _item_count, _sel, _scroll, [&](int row, int y, bool sel, int reserve) {
@@ -591,7 +591,7 @@ class SettingsScreen : public UIScreen {
int renderScopeMgmt(DisplayDriver& display) {
display.setColor(DisplayDriver::LIGHT);
display.drawCenteredHeader("SCOPE", true, _scope_action_menu.active);
drawScreenHeader(display, "Scope", -1, 0, true, _scope_action_menu.active);
const ScopeList& sl = the_mesh.scopeList();
int total = sl.totalCount() + 1; // +1 synthetic "+ Add scope" row
drawList(display, total, _scope_mgmt_sel, _scope_mgmt_scroll, [&](int idx, int y, bool sel, int reserve) {
@@ -651,18 +651,17 @@ public:
if (_scope_mgmt_active) return renderScopeMgmt(display);
display.drawCenteredHeader("SETTINGS");
drawScreenHeader(display, "Settings");
int mq_delay = 0;
_acc.render(display,
// Section header: "[+/-] Name"
// Section header: disclosure mark + name
[&](int sec, int y, bool sel, int reserve, bool collapsed) {
_reserve = reserve;
display.setColor(DisplayDriver::LIGHT);
drawRowSelection(display, y, sel, reserve);
display.setCursor(2, y);
display.print(collapsed ? "+" : "-");
display.print(" ");
drawDisclosure(display, 2, y, !collapsed);
display.setCursor(2 + 2 * display.getCharWidth(), y);
display.print(sectionName(_sec_header[sec]));
},
// Item row
+8 -11
View File
@@ -21,7 +21,7 @@ class ToolsScreen : public UIScreen {
#endif
};
struct Tool { const char* label; const MiniIcon* icon; Action action; };
struct Section { const char* name; const MiniIcon* icon; const Tool* tools; uint8_t count; };
struct Section { const char* name; const Tool* tools; uint8_t count; };
static const Tool LOCATION_TOOLS[];
static const Tool COMMS_TOOLS[];
@@ -79,20 +79,17 @@ public:
int render(DisplayDriver& display) override {
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
display.drawCenteredHeader("TOOLS");
drawScreenHeader(display, "Tools");
const int cw = display.getCharWidth();
const int g = gutter(display);
_acc.render(display,
// Section header: "[+/-] <icon> Name"
// Section header: disclosure mark + name
[&](int sec, int y, bool sel, int reserve, bool collapsed) {
drawRowSelection(display, y, sel, reserve);
display.setCursor(2, y);
display.print(collapsed ? "+" : "-");
const int icon_x = 2 + cw + 2;
drawIcon(display, icon_x, y, SECTIONS[sec].icon);
display.setCursor(icon_x + g, y);
drawDisclosure(display, 2, y, !collapsed);
display.setCursor(2 + 2 * cw, y); // the mark stands in for an icon, as in Settings
display.print(SECTIONS[sec].name);
},
// Item: indented "<icon> Label"
@@ -142,7 +139,7 @@ const ToolsScreen::Tool ToolsScreen::SYSTEM_TOOLS[] = {
#endif
};
const ToolsScreen::Section ToolsScreen::SECTIONS[] = {
{ "Location", &ICON_MAP_CONTACT, LOCATION_TOOLS, (uint8_t)(sizeof(LOCATION_TOOLS)/sizeof(LOCATION_TOOLS[0])) },
{ "Comms", &ICON_ADVERT, COMMS_TOOLS, 4 },
{ "System", &ICON_GEAR, SYSTEM_TOOLS, (uint8_t)(sizeof(SYSTEM_TOOLS)/sizeof(SYSTEM_TOOLS[0])) },
{ "Location", LOCATION_TOOLS, (uint8_t)(sizeof(LOCATION_TOOLS)/sizeof(LOCATION_TOOLS[0])) },
{ "Comms", COMMS_TOOLS, 4 },
{ "System", SYSTEM_TOOLS, (uint8_t)(sizeof(SYSTEM_TOOLS)/sizeof(SYSTEM_TOOLS[0])) },
};
@@ -124,14 +124,12 @@ public:
// Waypoint management UI owns the screen while active.
if (_wp.active()) return _wp.render(display);
// Title carries the view counter so the bottom hint row can be reclaimed
// for content.
const char* base = (_view == V_MAP) ? (_map_grid ? "TRAIL MAP+" : "TRAIL MAP")
: (_view == V_LIST) ? "TRAIL LIST"
: "TRAIL";
char title[20];
snprintf(title, sizeof(title), "%s %d/%d", base, (int)_view + 1, (int)V_COUNT);
display.drawCenteredHeader(title);
// The title bar carries the view dots so the bottom hint row can be
// reclaimed for content.
const char* title = (_view == V_MAP) ? (_map_grid ? "Trail map+" : "Trail map")
: (_view == V_LIST) ? "Trail list"
: "Trail";
drawScreenHeader(display, title, (int)_view, (int)V_COUNT);
if (_view == V_MAP) renderMap(display);
else if (_view == V_LIST) renderList(display);
+23 -17
View File
@@ -14,6 +14,7 @@
#include "PopupMenu.h"
#include "NavView.h"
#include "DigitEditor.h"
#include "InfoKit.h"
#include "../ui-core/GpsAverager.h"
#include "../ui-core/TrackBack.h"
@@ -140,28 +141,33 @@ class WaypointsView {
void renderAddForm(DisplayDriver& display) {
display.setColor(DisplayDriver::LIGHT);
display.drawCenteredHeader("ADD WAYPOINT");
drawScreenHeader(display, "Add waypoint");
const int top = display.listStart();
const int step = display.lineStep();
for (int i = 0; i < 4; i++) {
int y = top + i * step;
bool sel = (i == _add_sel);
if (i == 3) { // the form's button
drawButton(display, (display.width() - buttonWidth(display, "Save")) / 2, y, "Save", sel);
continue;
}
display.drawSelectionRow(0, y - 1, display.width(), step - 1, sel);
// While editing a magnitude, draw the row prefix + the digit editor so the
// place under the cursor is highlighted; otherwise draw the plain value.
const char* label = i == 0 ? "Lat" : i == 1 ? "Lon" : "Label";
const char hemi = i == 0 ? (_add_lat_neg ? 'S' : 'N') : (_add_lon_neg ? 'W' : 'E');
// While editing a magnitude, draw the hemisphere + the digit editor so
// the place under the cursor is highlighted; otherwise the plain value.
if ((i == 0 || i == 1) && sel && _add_editor.active) {
char prefix[10];
if (i == 0) snprintf(prefix, sizeof(prefix), "Lat: %c ", _add_lat_neg ? 'S' : 'N');
else snprintf(prefix, sizeof(prefix), "Lon: %c ", _add_lon_neg ? 'W' : 'E');
display.setCursor(2, y); display.print(prefix);
_add_editor.render(display, 2 + (int)strlen(prefix) * display.getCharWidth(), y);
const int right = display.width() - 2;
const int ex = right - display.getTextWidth(i == 0 ? "00.00000" : "000.00000");
display.setCursor(2, y); display.print(label);
char h[3] = { hemi, ' ', 0 };
display.setCursor(ex - display.getTextWidth(h), y); display.print(h);
_add_editor.render(display, ex, y);
} else {
char row[28];
if (i == 0) snprintf(row, sizeof(row), "Lat: %c %.5f", _add_lat_neg ? 'S' : 'N', _add_lat_mag);
else if (i == 1) snprintf(row, sizeof(row), "Lon: %c %.5f", _add_lon_neg ? 'W' : 'E', _add_lon_mag);
else if (i == 2) snprintf(row, sizeof(row), "Label: %s", _add_label[0] ? _add_label : "(auto)");
else snprintf(row, sizeof(row), "[Save]");
display.setCursor(2, y); display.print(row);
char val[24];
if (i < 2) snprintf(val, sizeof(val), "%c %.5f", hemi, i == 0 ? _add_lat_mag : _add_lon_mag);
else snprintf(val, sizeof(val), "%s", _add_label[0] ? _add_label : "(auto)");
info::valueRow(display, y, label, val, sel, 0);
}
display.setColor(DisplayDriver::LIGHT);
}
@@ -171,7 +177,7 @@ class WaypointsView {
// reports remaining time and the running sample count.
void renderAvg(DisplayDriver& display) {
display.setColor(DisplayDriver::LIGHT);
display.drawCenteredHeader("AVERAGING GPS");
drawScreenHeader(display, "Averaging GPS");
const int top = display.listStart();
const int step = display.lineStep();
char line[28];
@@ -188,9 +194,9 @@ class WaypointsView {
int mq_delay = 0;
display.setColor(DisplayDriver::LIGHT);
char title[24];
snprintf(title, sizeof(title), "WAYPOINTS %d/%d",
snprintf(title, sizeof(title), "Waypoints %d/%d",
_task->waypoints().count(), WaypointStore::CAPACITY);
display.drawCenteredHeader(title);
drawScreenHeader(display, title);
int n = wpListCount();
int total = n + 1; // final row = "+ Add by coords"
+65
View File
@@ -965,3 +965,68 @@ inline void drawCheckbox(DisplayDriver& d, int x, int y, bool on) {
d.drawRect(x, y, box, box);
if (on) d.fillRect(x + 2, y + 2, box - 4, box - 4);
}
// Page dots for a screen with views, right edge at x_right, centred on cy:
// the current page a filled square, the others a point.
inline int pageDotsWidth(DisplayDriver& d, int pages) {
const int s = miniIconScale(d);
return pages > 1 ? (pages - 1) * 5 * s + 3 * s : 0;
}
inline void drawPageDots(DisplayDriver& d, int x_right, int cy, int page, int pages) {
const int s = miniIconScale(d);
for (int i = 0; i < pages; i++) {
const int cx = x_right - 2 * s - (pages - 1 - i) * 5 * s; // centre of dot i
if (i == page) d.fillRect(cx - s, cy - s, 3 * s, 3 * s);
else d.fillRect(cx, cy, s, s);
}
}
// A screen's title bar: the title centred over the separator line, page
// dots at the right when the screen has pages, and the ≡ hint when it has a
// Hold-Enter menu (menu_open highlights it). A title too long to centre goes
// left and ends in an ellipsis. No icon: the screen was just picked by one.
// Leaves the ink LIGHT.
inline void drawScreenHeader(DisplayDriver& d, const char* title, int page = -1, int pages = 0,
bool menu_hint = false, bool menu_open = false) {
const int s = miniIconScale(d);
d.setColor(DisplayDriver::LIGHT);
const int hint = menu_hint ? d.menuHintWidth() : 0;
const int dots = pages > 1 ? pageDotsWidth(d, pages) + 3 * s : 0;
const int reserve = hint + dots;
char buf[96];
d.translateUTF8ToBlocks(buf, (title && title[0]) ? title : "", sizeof(buf));
const int tw = d.getTextWidth(buf), avail = d.width() - reserve - 4;
if (tw <= avail) {
int x = d.width() / 2 - tw / 2; // centred on the screen
if (x + tw > d.width() - reserve - 2) x = (d.width() - reserve) / 2 - tw / 2; // or clear of dots / ≡
d.setCursor(x, 0);
d.print(buf);
} else {
d.drawTextEllipsized(2, 0, avail > 0 ? avail : 0, buf);
}
d.fillRect(0, d.headerH() - d.sepH(), d.width(), d.sepH());
if (pages > 1) drawPageDots(d, d.width() - hint - s, (d.headerH() - d.sepH()) / 2, page, pages);
if (menu_hint) d.drawContextMenuHint(DisplayDriver::LIGHT, menu_open);
}
// An accordion section's disclosure mark, one character wide: a triangle
// pointing right while the section is folded, down while it's open.
inline void drawDisclosure(DisplayDriver& d, int x, int y, bool open) {
const int s = miniIconScale(d), cy = y + d.getLineHeight() / 2;
if (open) for (int i = 0; i < 3; i++) d.fillRect(x + i * s, cy - s + i * s, (5 - 2 * i) * s, s);
else for (int i = 0; i < 3; i++) d.fillRect(x + s + i * s, cy - 2 * s + i * s, s, (5 - 2 * i) * s);
}
// A push button: its label in a soft pill, filled while selected. One line
// tall (the labels have no descenders worth padding for). Returns its width.
inline int buttonWidth(DisplayDriver& d, const char* label) { return d.getTextWidth(label) + 6; }
inline int drawButton(DisplayDriver& d, int x, int y, const char* label, bool sel) {
const int w = buttonWidth(d, label), h = d.getLineHeight();
d.setColor(DisplayDriver::LIGHT);
if (sel) { d.fillSoftRect(x, y, w, h); d.setColor(DisplayDriver::DARK); }
else d.drawSoftRect(x, y, w, h);
d.setCursor(x + 3, y);
d.print(label);
d.setColor(DisplayDriver::LIGHT);
return w;
}