From 9da730e3b667c8eb87cc0e3985889cdd1ea057f3 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:23:00 +0200 Subject: [PATCH] refactor(ui): shared drawList + header + key-decode helpers - drawList(): scrollable list skeleton (visible window, keep-sel-in-view, scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot, Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the scroll-clamp/reserve/indicator that each had open-coded (and inconsistently). - drawCenteredHeader(): centred title + separator, replacing the duplicated two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and was missed before); unifies the separator at headerH()-sepH(). - keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom; applied to 6 screens, behaviour-preserving. Cancel/context-menu stay per-screen (they mean different things on different screens). - rotLabel() dedups the 0/90/180/270 array in Settings. - Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest). Co-Authored-By: Claude Opus 4.8 --- .../companion_radio/ui-new/AutoAdvertScreen.h | 7 ++-- examples/companion_radio/ui-new/BotScreen.h | 24 ++++------- .../companion_radio/ui-new/CompassScreen.h | 3 +- .../ui-new/DashboardConfigScreen.h | 7 ++-- .../companion_radio/ui-new/NearbyScreen.h | 18 +++----- .../companion_radio/ui-new/QuickMsgScreen.h | 41 +++++-------------- .../companion_radio/ui-new/SettingsScreen.h | 22 +++++----- examples/companion_radio/ui-new/ToolsScreen.h | 23 ++--------- examples/companion_radio/ui-new/TrailScreen.h | 3 +- .../companion_radio/ui-new/WaypointsView.h | 27 ++++-------- examples/companion_radio/ui-new/icons.h | 22 ++++++++++ src/helpers/ui/DisplayDriver.h | 9 ++++ src/helpers/ui/UIScreen.h | 6 +++ 13 files changed, 89 insertions(+), 123 deletions(-) diff --git a/examples/companion_radio/ui-new/AutoAdvertScreen.h b/examples/companion_radio/ui-new/AutoAdvertScreen.h index 50ca594c..f6c0a286 100644 --- a/examples/companion_radio/ui-new/AutoAdvertScreen.h +++ b/examples/companion_radio/ui-new/AutoAdvertScreen.h @@ -30,8 +30,7 @@ public: int bar_h = display.lineStep(); int tip_y = bar_y + bar_h + 4; - display.drawTextCentered(display.width() / 2, 0, "AUTO-ADVERT"); - display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); + display.drawCenteredHeader("AUTO-ADVERT"); display.setCursor(2, label_y); display.print("Interval:"); @@ -56,8 +55,8 @@ public: _task->gotoToolsScreen(); return true; } - bool right = (c == KEY_RIGHT || c == KEY_NEXT || c == KEY_ENTER); - bool left = (c == KEY_LEFT || c == KEY_PREV); + bool right = keyIsNext(c) || c == KEY_ENTER; + bool left = keyIsPrev(c); if (right || left) { int idx = currentIdx(); idx = right ? (idx + 1) % OPT_COUNT : (idx + OPT_COUNT - 1) % OPT_COUNT; diff --git a/examples/companion_radio/ui-new/BotScreen.h b/examples/companion_radio/ui-new/BotScreen.h index 157ebd9d..6028fc88 100644 --- a/examples/companion_radio/ui-new/BotScreen.h +++ b/examples/companion_radio/ui-new/BotScreen.h @@ -66,14 +66,9 @@ public: return _kb->render(display); } - int item_h = display.lineStep(); - int start_y = display.listStart(); - int val_x = display.valCol(); - _visible = display.listVisible(item_h); - if (_visible < 1) _visible = 1; - int reserve = scrollIndicatorReserve(display, ITEM_COUNT, _visible); + int val_x = display.valCol(); - display.drawTextCentered(display.width() / 2, 0, "AUTO-REPLY BOT"); + display.drawCenteredHeader("AUTO-REPLY BOT"); // reply counter, right-aligned in the header uint16_t sent = the_mesh.botReplyCount(); if (sent > 0) { @@ -83,16 +78,12 @@ public: display.setCursor(display.width() - cw - 1, 0); display.print(cbuf); } - display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); static const char* labels[] = { "Enable", "Channel", "Trigger DM", "Reply DM", "Trigger Ch", "Reply Ch", "Commands", "Quiet from", "Quiet to" }; - for (int vi = 0; vi < _visible && (_scroll + vi) < ITEM_COUNT; vi++) { - int i = _scroll + vi; - int y = start_y + vi * item_h; - bool sel = (i == _sel); - display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h, sel); + _visible = drawList(display, ITEM_COUNT, _sel, _scroll, [&](int i, int y, bool sel, int reserve) { + display.drawSelectionRow(0, y - 1, display.width() - reserve, display.lineStep() - 1, sel); display.setCursor(2, y); display.print(labels[i]); display.setCursor(val_x, y); @@ -131,16 +122,15 @@ public: } } display.setColor(DisplayDriver::LIGHT); - } - drawScrollIndicator(display, start_y, _visible * item_h, ITEM_COUNT, _visible, _scroll); + }); return 2000; } bool handleInput(char c) override { bool up = (c == KEY_UP); bool down = (c == KEY_DOWN); - bool left = (c == KEY_LEFT || c == KEY_PREV); - bool right = (c == KEY_RIGHT || c == KEY_NEXT); + bool left = keyIsPrev(c); + bool right = keyIsNext(c); bool enter = (c == KEY_ENTER); bool cancel = (c == KEY_CANCEL || c == KEY_CONTEXT_MENU); diff --git a/examples/companion_radio/ui-new/CompassScreen.h b/examples/companion_radio/ui-new/CompassScreen.h index b5b7e32b..163dd91c 100644 --- a/examples/companion_radio/ui-new/CompassScreen.h +++ b/examples/companion_radio/ui-new/CompassScreen.h @@ -31,8 +31,7 @@ public: int render(DisplayDriver& display) override { display.setTextSize(1); display.setColor(DisplayDriver::LIGHT); - display.drawTextCentered(display.width() / 2, 0, "COMPASS"); - display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); + display.drawCenteredHeader("COMPASS"); const int hdr = display.headerH(); const int cx = display.width() / 2; diff --git a/examples/companion_radio/ui-new/DashboardConfigScreen.h b/examples/companion_radio/ui-new/DashboardConfigScreen.h index c5ab03da..74b5aae2 100644 --- a/examples/companion_radio/ui-new/DashboardConfigScreen.h +++ b/examples/companion_radio/ui-new/DashboardConfigScreen.h @@ -46,8 +46,7 @@ public: int start_y = display.listStart(); int val_x = display.valCol(); - display.drawTextCentered(display.width() / 2, 0, "CLOCK FIELDS"); - display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); + display.drawCenteredHeader("CLOCK FIELDS"); static const char* labels[] = { "Field 1", "Field 2", "Field 3" }; for (int i = 0; i < FIELD_SLOTS; i++) { @@ -72,8 +71,8 @@ public: } if (c == KEY_UP && _sel > 0) { _sel--; return true; } if (c == KEY_DOWN && _sel < FIELD_SLOTS - 1){ _sel++; return true; } - if (c == KEY_LEFT || c == KEY_PREV) { cycle(_sel, -1); return true; } - if (c == KEY_RIGHT || c == KEY_NEXT) { cycle(_sel, 1); return true; } + if (keyIsPrev(c)) { cycle(_sel, -1); return true; } + if (keyIsNext(c)) { cycle(_sel, 1); return true; } if (c == KEY_ENTER) { cycle(_sel, 1); return true; } return false; } diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index 074305d1..d7c8bad8 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -569,8 +569,7 @@ public: snprintf(title, sizeof(title), "NEARBY %s %s", FILTER_LABELS[_filter], _sort == SORT_TIME ? "recent" : "dist"); } - display.drawTextCentered(display.width() / 2, 0, title); - display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); + display.drawCenteredHeader(title); if (_count == 0) { char empty[24]; @@ -588,11 +587,7 @@ public: display.drawTextCentered(display.width() / 2, display.height() / 2 + display.lineStep() / 2, hint); } } else { - int reserve = scrollIndicatorReserve(display, _count, _visible); - for (int i = 0; i < _visible && (_scroll + i) < _count; i++) { - int idx = _scroll + i; - bool sel = (idx == _sel); - int y = start_y + i * item_h; + _visible = drawList(display, _count, _sel, _scroll, [&](int idx, int y, bool sel, int reserve) { const Entry& e = _entries[idx]; display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h - 1, sel); @@ -623,9 +618,7 @@ public: } display.setCursor(display.width() - display.getTextWidth(right) - 2 - reserve, y); display.print(right); - } - - drawScrollIndicator(display, start_y, _visible * item_h, _count, _visible, _scroll); + }); } if (renderActivePopup(display)) return 50; @@ -636,8 +629,7 @@ public: bool handleInput(char c) override { // ── navigate-to-node view — any nav key returns to detail ───────────────── if (_nav) { - if (c == KEY_CANCEL || c == KEY_LEFT || c == KEY_PREV || - c == KEY_RIGHT || c == KEY_NEXT) _nav = false; + if (c == KEY_CANCEL || keyIsPrev(c) || keyIsNext(c)) _nav = false; return true; } @@ -647,7 +639,7 @@ public: // LEFT/RIGHT on the Sort row toggles the value in-place and rebuilds the // label; the popup stays open so the user can keep tapping. Other rows // swallow L/R. ENTER on Sort just closes (value changes via L/R only). - if (c == KEY_LEFT || c == KEY_RIGHT || c == KEY_PREV || c == KEY_NEXT) { + if (keyIsPrev(c) || keyIsNext(c)) { int i = _menu.selectedIndex(); if (i >= 0 && i < _menu_action_count && _menu_actions[i] == ACT_SORT) { _sort = (_sort == SORT_DIST) ? SORT_TIME : SORT_DIST; diff --git a/examples/companion_radio/ui-new/QuickMsgScreen.h b/examples/companion_radio/ui-new/QuickMsgScreen.h index a27ed655..b58b0af3 100644 --- a/examples/companion_radio/ui-new/QuickMsgScreen.h +++ b/examples/companion_radio/ui-new/QuickMsgScreen.h @@ -822,8 +822,7 @@ public: _visible = display.listVisible(item_h); if (_phase == MODE_SELECT) { - display.drawTextCentered(display.width()/2, 0, "MESSAGE"); - display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); + display.drawCenteredHeader("MESSAGE"); const char* opts[] = { "Direct message", "Channels", "Room Servers" }; int badges[3] = { getDMUnreadTotal(), @@ -848,20 +847,15 @@ public: if (_ctx_menu.active) _ctx_menu.render(display); } else if (_phase == CONTACT_PICK) { - display.drawTextCentered(display.width()/2, 0, _room_mode ? "SELECT ROOM" : "SELECT CONTACT"); - display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); + display.drawCenteredHeader(_room_mode ? "SELECT ROOM" : "SELECT CONTACT"); if (_num_contacts == 0) { display.drawTextCentered(display.width()/2, display.height()/2, _room_mode ? "No room servers" : "No favourites"); return 5000; } - int reserve = scrollIndicatorReserve(display, _num_contacts, _visible); - for (int i = 0; i < _visible && (_contact_scroll+i) < _num_contacts; i++) { - int list_idx = _contact_scroll + i; + _visible = drawList(display, _num_contacts, _contact_sel, _contact_scroll, [&](int list_idx, int y, bool sel, int reserve) { int mesh_idx = _sorted[list_idx]; - bool sel = (list_idx == _contact_sel); - int y = start_y + i * item_h; display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h - 1, sel); @@ -882,27 +876,20 @@ public: display.print(badge); } } - } - drawScrollIndicator(display, start_y, _visible * item_h, _num_contacts, _visible, _contact_scroll); + }); // Context menu overlay if (_ctx_menu.active) _ctx_menu.render(display); } else if (_phase == CHANNEL_PICK) { - display.drawTextCentered(display.width()/2, 0, "SELECT CHANNEL"); - display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); + display.drawCenteredHeader("SELECT CHANNEL"); if (_num_channels == 0) { display.drawTextCentered(display.width()/2, display.height()/2, "No channels"); return 5000; } - int reserve = scrollIndicatorReserve(display, _num_channels, _visible); - for (int i = 0; i < _visible && (_channel_scroll+i) < _num_channels; i++) { - int list_idx = _channel_scroll + i; - bool sel = (list_idx == _channel_sel); - int y = start_y + i * item_h; - + _visible = drawList(display, _num_channels, _channel_sel, _channel_scroll, [&](int list_idx, int y, bool sel, int reserve) { display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h - 1, sel); ChannelDetails ch; if (the_mesh.getChannel(_channel_indices[list_idx], ch)) { @@ -919,8 +906,7 @@ public: display.print(badge); } } - } - drawScrollIndicator(display, start_y, _visible * item_h, _num_channels, _visible, _channel_scroll); + }); // Context menu overlay if (_ctx_menu.active) _ctx_menu.render(display); @@ -1244,16 +1230,10 @@ public: } else { snprintf(title, sizeof(title), "TO:%.14s", _sel_contact.name); } - display.drawTextCentered(display.width()/2, 0, title); - display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); + display.drawCenteredHeader(title); int total_msg_items = 1 + _active_msg_count; - int reserve = scrollIndicatorReserve(display, total_msg_items, _visible); - for (int i = 0; i < _visible && (_msg_scroll+i) < total_msg_items; i++) { - int idx = _msg_scroll + i; - bool sel = (idx == _msg_sel); - int y = start_y + i * item_h; - + _visible = drawList(display, total_msg_items, _msg_sel, _msg_scroll, [&](int idx, int y, bool sel, int reserve) { display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h - 1, sel); if (idx == 0) { @@ -1265,8 +1245,7 @@ public: const char* tmpl = p ? p->custom_msgs[slot] : ""; display.drawTextEllipsized(2, y, display.width() - 4 - reserve, tmpl); } - } - drawScrollIndicator(display, start_y, _visible * item_h, total_msg_items, _visible, _msg_scroll); + }); } return 2000; } diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 5a5005c2..2b75e644 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -115,6 +115,12 @@ class SettingsScreen : public UIScreen { // values never render under the indicator when the list scrolls. int valCol(DisplayDriver& display) const { return display.valCol() - _reserve; } + // Shared 0/90/180/270 labels for display + joystick rotation. + static const char* rotLabel(uint8_t r) { + static const char* const L[] = { "0 deg", "90 deg", "180 deg", "270 deg" }; + return L[r & 3]; + } + void renderBar(DisplayDriver& display, int x, int y, int value, int max_val) { const int gap = 2; const int avail = display.width() - x - _reserve; @@ -540,17 +546,13 @@ class SettingsScreen : public UIScreen { } else if (item == ROTATION) { display.print("Rotation"); display.setCursor(valCol(display), y); - { static const char* ROT_LABELS[] = { "0 deg", "90 deg", "180 deg", "270 deg" }; - uint8_t r = p ? (p->display_rotation & 3) : 0; - display.print(ROT_LABELS[r]); } + display.print(rotLabel(p ? p->display_rotation : 0)); #endif #if FEAT_JOYSTICK_ROTATION_SETTING } else if (item == JOY_ROTATION) { display.print("Joystick"); display.setCursor(valCol(display), y); - { static const char* ROT_LABELS[] = { "0 deg", "90 deg", "180 deg", "270 deg" }; - uint8_t r = p ? (p->joystick_rotation & 3) : 0; - display.print(ROT_LABELS[r]); } + display.print(rotLabel(p ? p->joystick_rotation : 0)); #endif #if FEAT_FULL_REFRESH_SETTING } else if (item == EINK_FULL_REFRESH) { @@ -620,9 +622,7 @@ public: _visible = display.listVisible(item_h); _reserve = scrollIndicatorReserve(display, _vis_count, _visible); - display.setColor(DisplayDriver::LIGHT); - display.drawTextCentered(display.width() / 2, 0, "SETTINGS"); - display.fillRect(0, display.headerH() - display.sepH(), display.width(), display.sepH()); + display.drawCenteredHeader("SETTINGS"); for (int i = 0; i < _visible && (_scroll + i) < _vis_count; i++) { renderItem(display, _vis[_scroll + i], start_y + i * item_h); @@ -676,8 +676,8 @@ public: return true; } - bool right = (c == KEY_RIGHT || c == KEY_NEXT); - bool left = (c == KEY_LEFT || c == KEY_PREV); + bool right = keyIsNext(c); + bool left = keyIsPrev(c); bool enter = (c == KEY_ENTER); if (enter && isSection(_selected)) { diff --git a/examples/companion_radio/ui-new/ToolsScreen.h b/examples/companion_radio/ui-new/ToolsScreen.h index 2c9bc654..7479810a 100644 --- a/examples/companion_radio/ui-new/ToolsScreen.h +++ b/examples/companion_radio/ui-new/ToolsScreen.h @@ -16,28 +16,13 @@ public: int render(DisplayDriver& display) override { display.setTextSize(1); display.setColor(DisplayDriver::LIGHT); - display.drawTextCentered(display.width() / 2, 0, "TOOLS"); - display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); + display.drawCenteredHeader("TOOLS"); - int item_h = display.lineStep(); - int start_y = display.listStart(); - int vis = display.listVisible(item_h); - if (vis < 1) vis = 1; - - // Keep the selection in view (short OLED panel can't show all 6 items). - if (_sel < _scroll) _scroll = _sel; - if (_sel >= _scroll + vis) _scroll = _sel - vis + 1; - - int reserve = scrollIndicatorReserve(display, ITEM_COUNT, vis); - for (int i = 0; i < vis && (_scroll + i) < ITEM_COUNT; i++) { - int idx = _scroll + i; - int y = start_y + i * item_h; - bool sel = (idx == _sel); - display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h, sel); + drawList(display, ITEM_COUNT, _sel, _scroll, [&](int idx, int y, bool sel, int reserve) { + display.drawSelectionRow(0, y - 1, display.width() - reserve, display.lineStep() - 1, sel); display.setCursor(2, y); display.print(ITEMS[idx]); - } - drawScrollIndicator(display, start_y, vis * item_h, ITEM_COUNT, vis, _scroll); + }); return 500; } diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 5466b603..5330f19b 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -76,8 +76,7 @@ public: : "TRAIL"; char title[20]; snprintf(title, sizeof(title), "%s %d/%d", base, (int)_view + 1, (int)V_COUNT); - display.drawTextCentered(display.width() / 2, 0, title); - display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); + display.drawCenteredHeader(title); 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 5f63390e..33d28c34 100644 --- a/examples/companion_radio/ui-new/WaypointsView.h +++ b/examples/companion_radio/ui-new/WaypointsView.h @@ -127,8 +127,7 @@ class WaypointsView { void renderAddForm(DisplayDriver& display) { display.setColor(DisplayDriver::LIGHT); - display.drawTextCentered(display.width() / 2, 0, "ADD WAYPOINT"); - display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); + display.drawCenteredHeader("ADD WAYPOINT"); const int top = display.listStart(); const int step = display.lineStep(); for (int i = 0; i < 4; i++) { @@ -150,35 +149,25 @@ class WaypointsView { char title[24]; snprintf(title, sizeof(title), "WAYPOINTS %d/%d", _task->waypoints().count(), WaypointStore::CAPACITY); - display.drawTextCentered(display.width() / 2, 0, title); - display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); + display.drawCenteredHeader(title); int n = wpListCount(); int total = n + 1; // final row = "+ Add by coords" - const int top = display.listStart(); const int step = display.lineStep(); - int vis = display.listVisible(); - if (vis < 1) vis = 1; - if (_sel < _scroll) _scroll = _sel; - if (_sel >= _scroll + vis) _scroll = _sel - vis + 1; int32_t mylat, mylon; bool have = ownPos(mylat, mylon); - int reserve = scrollIndicatorReserve(display, total, vis); - for (int i = 0; i < vis && (_scroll + i) < total; i++) { - int row = _scroll + i; - int y = top + i * step; - bool sel = (row == _sel); + drawList(display, total, _sel, _scroll, [&](int row, int y, bool sel, int reserve) { display.drawSelectionRow(0, y - 1, display.width() - reserve, step - 1, sel); if (row == n) { // the synthetic "Add" row display.setCursor(2, y); display.print("+ Add by coords"); display.setColor(DisplayDriver::LIGHT); - continue; + return; } int32_t tlat, tlon; const char* label; - if (!rowTarget(row, tlat, tlon, label)) continue; + if (!rowTarget(row, tlat, tlon, label)) return; char dist[12] = ""; int bw = 0; if (have) { geo::fmtDist(dist, sizeof(dist), geo::haversineKm(mylat, mylon, tlat, tlon), useImperial()); @@ -189,8 +178,7 @@ class WaypointsView { display.drawTextEllipsized(2, y, display.width() - 2 - bw - reserve, nm); if (dist[0]) { display.setCursor(display.width() - bw + 1 - reserve, y); display.print(dist); } display.setColor(DisplayDriver::LIGHT); - } - drawScrollIndicator(display, top, vis * step, total, vis, _scroll); + }); } void renderWpNav(DisplayDriver& display) { @@ -322,8 +310,7 @@ public: // Navigation view — any nav key returns to the list. if (_mode == NAV) { - if (c == KEY_CANCEL || c == KEY_LEFT || c == KEY_PREV || - c == KEY_RIGHT || c == KEY_NEXT) { _mode = LIST; } + if (c == KEY_CANCEL || keyIsPrev(c) || keyIsNext(c)) { _mode = LIST; } return true; } diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index d1875523..6b58d986 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -276,6 +276,28 @@ inline void drawScrollIndicator(DisplayDriver& d, int top_y, int track_h, miniIconDrawHalo(d, x, top_y + track_h - th, ICON_SCROLL_DOWN); } +// Scrollable item-list skeleton shared by the list screens. Computes the visible +// window from the font metrics, keeps `sel` in view, reserves the scroll-column, +// draws each visible row through `row`, then the indicator. The callback +// `row(idx, y, sel, reserve)` draws one item — including its own selection bar — +// using `reserve` to keep right-aligned content clear of the indicator. Returns +// the visible row count (callers cache it for input handling). +template +inline int drawList(DisplayDriver& d, int total, int sel, int& scroll, RenderRow row) { + const int item_h = d.lineStep(); + const int start_y = d.listStart(); + int visible = d.listVisible(item_h); + if (visible < 1) visible = 1; + if (sel < scroll) scroll = sel; + if (sel >= scroll + visible) scroll = sel - visible + 1; + if (scroll < 0) scroll = 0; + const int reserve = scrollIndicatorReserve(d, total, visible); + for (int i = 0; i < visible && (scroll + i) < total; i++) + row(scroll + i, start_y + i * item_h, scroll + i == sel, reserve); + drawScrollIndicator(d, start_y, visible * item_h, total, visible, scroll); + return visible; +} + // ── Big ASCII-art icons (skeleton, not yet used) ───────────────────────────── // Same authoring idea as the mini-icons but for full page glyphs up to 32 px // wide: one uint32_t per row. The existing XBM bitmaps below (logo/bluetooth/…) diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 85f9f709..3ad36847 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -185,6 +185,15 @@ public: fillRect(0, hdr - 1, width(), sepH()); } + // Centred screen title + bottom separator line — the standard top bar for + // full-screen list/detail views. Leaves ink LIGHT; callers can draw extra + // header content (counters, etc.) afterwards. + void drawCenteredHeader(const char* title) { + setColor(LIGHT); + drawTextCentered(width() / 2, 0, title); + fillRect(0, headerH() - sepH(), width(), sepH()); + } + // Advance a UTF-8 pointer by one codepoint, returning the decoded value. // Invalid sequences return 0xFFFD and consume trailing continuation bytes. static uint32_t decodeCodepoint(const uint8_t*& p) { diff --git a/src/helpers/ui/UIScreen.h b/src/helpers/ui/UIScreen.h index d78c9124..bdde8e04 100644 --- a/src/helpers/ui/UIScreen.h +++ b/src/helpers/ui/UIScreen.h @@ -14,6 +14,12 @@ #define KEY_PREV 0xF2 #define KEY_CONTEXT_MENU 0xF3 +// "Previous"/"Next" navigation keys: the directional key and its rotary-encoder +// twin. Decrement / increment a value, or step a selection. (Cancel and the +// context-menu key stay screen-specific — they mean different things per screen.) +static inline bool keyIsPrev(char c) { return c == KEY_LEFT || c == KEY_PREV; } +static inline bool keyIsNext(char c) { return c == KEY_RIGHT || c == KEY_NEXT; } + #ifndef JOYSTICK_ROTATION #define JOYSTICK_ROTATION 0 #endif