diff --git a/examples/companion_radio/ui-new/AdminScreen.h b/examples/companion_radio/ui-new/AdminScreen.h index 389bee2b..0beed646 100644 --- a/examples/companion_radio/ui-new/AdminScreen.h +++ b/examples/companion_radio/ui-new/AdminScreen.h @@ -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"); } diff --git a/examples/companion_radio/ui-new/AutoAdvertScreen.h b/examples/companion_radio/ui-new/AutoAdvertScreen.h index d117e261..0aefe461 100644 --- a/examples/companion_radio/ui-new/AutoAdvertScreen.h +++ b/examples/companion_radio/ui-new/AutoAdvertScreen.h @@ -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); diff --git a/examples/companion_radio/ui-new/ChannelsView.h b/examples/companion_radio/ui-new/ChannelsView.h index 6131cecb..59440ed1 100644 --- a/examples/companion_radio/ui-new/ChannelsView.h +++ b/examples/companion_radio/ui-new/ChannelsView.h @@ -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; diff --git a/examples/companion_radio/ui-new/ClockToolsScreen.h b/examples/companion_radio/ui-new/ClockToolsScreen.h index 76450032..fba5945a 100644 --- a/examples/companion_radio/ui-new/ClockToolsScreen.h +++ b/examples/companion_radio/ui-new/ClockToolsScreen.h @@ -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 diff --git a/examples/companion_radio/ui-new/CompassScreen.h b/examples/companion_radio/ui-new/CompassScreen.h index 79327e4a..8477622a 100644 --- a/examples/companion_radio/ui-new/CompassScreen.h +++ b/examples/companion_radio/ui-new/CompassScreen.h @@ -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(); diff --git a/examples/companion_radio/ui-new/DashboardConfigScreen.h b/examples/companion_radio/ui-new/DashboardConfigScreen.h index d46b202c..f21eb086 100644 --- a/examples/companion_radio/ui-new/DashboardConfigScreen.h +++ b/examples/companion_radio/ui-new/DashboardConfigScreen.h @@ -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++) { diff --git a/examples/companion_radio/ui-new/GpioScreen.h b/examples/companion_radio/ui-new/GpioScreen.h index b100fe4e..dc64609a 100644 --- a/examples/companion_radio/ui-new/GpioScreen.h +++ b/examples/companion_radio/ui-new/GpioScreen.h @@ -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) { diff --git a/examples/companion_radio/ui-new/KeyboardWidget.h b/examples/companion_radio/ui-new/KeyboardWidget.h index af89b306..56b03d86 100644 --- a/examples/companion_radio/ui-new/KeyboardWidget.h +++ b/examples/companion_radio/ui-new/KeyboardWidget.h @@ -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"); diff --git a/examples/companion_radio/ui-new/LiveShareScreen.h b/examples/companion_radio/ui-new/LiveShareScreen.h index 5f70d9e8..33152424 100644 --- a/examples/companion_radio/ui-new/LiveShareScreen.h +++ b/examples/companion_radio/ui-new/LiveShareScreen.h @@ -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) { diff --git a/examples/companion_radio/ui-new/LocatorScreen.h b/examples/companion_radio/ui-new/LocatorScreen.h index e0886315..4fb703dc 100644 --- a/examples/companion_radio/ui-new/LocatorScreen.h +++ b/examples/companion_radio/ui-new/LocatorScreen.h @@ -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) { diff --git a/examples/companion_radio/ui-new/MessagesScreen.h b/examples/companion_radio/ui-new/MessagesScreen.h index 6cdca723..a805d431 100644 --- a/examples/companion_radio/ui-new/MessagesScreen.h +++ b/examples/companion_radio/ui-new/MessagesScreen.h @@ -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) { diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index 6369522c..b288da48 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -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); } diff --git a/examples/companion_radio/ui-new/RepeaterScreen.h b/examples/companion_radio/ui-new/RepeaterScreen.h index 235de97f..50d61755 100644 --- a/examples/companion_radio/ui-new/RepeaterScreen.h +++ b/examples/companion_radio/ui-new/RepeaterScreen.h @@ -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) { diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index eadf876c..2170f024 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -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 diff --git a/examples/companion_radio/ui-new/ToolsScreen.h b/examples/companion_radio/ui-new/ToolsScreen.h index 529e91b4..edfd2655 100644 --- a/examples/companion_radio/ui-new/ToolsScreen.h +++ b/examples/companion_radio/ui-new/ToolsScreen.h @@ -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: "[+/-] 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 " 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])) }, }; diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 1a445cc7..df18593a 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -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); diff --git a/examples/companion_radio/ui-new/WaypointsView.h b/examples/companion_radio/ui-new/WaypointsView.h index 7a7bdb32..b3948bf9 100644 --- a/examples/companion_radio/ui-new/WaypointsView.h +++ b/examples/companion_radio/ui-new/WaypointsView.h @@ -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" diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index 13386f8b..7b7d9fab 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -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; +}