Files
MeshCore-Solo/examples/companion_radio/ui-new/PopupMenu.h
MarekZegare4 4145185031 chore: address code-audit hygiene (stale refs, dead TODO, popup scroll)
- FEATURES audit: refresh references the Nearby/Trail refactors made stale —
  M4 (renderDiscoverDetail → renderScanDetail), L1 (SNR %.1f now in scan detail
  + ping; list cards show RSSI), Trail grid (round-step renderGrid + MIN_GRID_PX).
- MyMesh: drop the obsolete `// TODO: add expected ACK to table` — the code
  right below already populates expected_ack_table.
- PopupMenu: render() is now the single source of truth for _scroll, clamping
  the selection into view from the live-height cap, so handleInput() only moves
  _sel and never depends on a stale _cap (M3). Behaviour-preserving.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 10:00:48 +02:00

101 lines
3.9 KiB
C

#pragma once
#include <helpers/ui/DisplayDriver.h>
#include <Arduino.h>
// Generic scrollable popup menu overlay.
// Caller owns item string lifetimes — addItem() stores const char* pointers.
// Navigation wraps around: up from first item goes to last, and vice versa.
struct PopupMenu {
static const int PM_MAX_ITEMS = 16;
static const int PM_BX = 12;
static const int PM_BY = 10;
static const int PM_BW = 104;
static const int PM_ITEM_H = 10;
const char* _items[PM_MAX_ITEMS];
int _count;
int _sel;
int _scroll;
int _visible; // caller hint (min visible items)
int _cap; // actual visible cap, updated each render()
bool active;
const char* _title;
enum Result { NONE, SELECTED, CANCELLED };
PopupMenu() : _count(0), _sel(0), _scroll(0), _visible(3), _cap(3), active(false), _title(nullptr) {}
void begin(const char* title, int visible = 3) {
_count = 0; _sel = 0; _scroll = 0;
_visible = visible; _cap = visible; active = true; _title = title;
}
void addItem(const char* item) {
if (_count < PM_MAX_ITEMS) _items[_count++] = item;
}
int render(DisplayDriver& display) {
// Hard ceiling: never show more items than physically fit on screen.
// On tall displays (portrait e-ink) this expands beyond _visible;
// on small displays (OLED 64px) it clamps below _visible.
int max_by_height = (display.height() - PM_BY - 12) / PM_ITEM_H;
if (max_by_height < 1) max_by_height = 1;
_cap = max_by_height;
int vis = (_count < _cap) ? _count : _cap;
// render() is the single source of truth for _scroll: using the cap just
// computed from the live display height, keep the selection inside the
// visible window. handleInput() therefore never has to touch _scroll (and
// can't act on a stale _cap from before the first render).
if (_sel < _scroll) _scroll = _sel;
else if (_sel >= _scroll + vis) _scroll = _sel - vis + 1;
int max_scroll = _count - vis;
if (max_scroll < 0) max_scroll = 0;
if (_scroll > max_scroll) _scroll = max_scroll;
if (_scroll < 0) _scroll = 0;
int bh = 12 + vis * PM_ITEM_H;
display.setColor(DisplayDriver::DARK);
display.fillRect(PM_BX, PM_BY, PM_BW, bh);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(PM_BX, PM_BY, PM_BW, bh);
display.setCursor(PM_BX + 4, PM_BY + 1);
display.print(_title);
display.fillRect(PM_BX, PM_BY + 10, PM_BW, 1);
if (_scroll > 0)
{ display.setCursor(PM_BX + PM_BW - 7, PM_BY + 1); display.print("^"); }
if (_scroll + vis < _count)
{ display.setCursor(PM_BX + PM_BW - 7, PM_BY + 2 + vis * PM_ITEM_H); display.print("v"); }
for (int i = 0; i < vis && (_scroll + i) < _count; i++) {
int idx = _scroll + i;
int py = PM_BY + 12 + i * PM_ITEM_H;
if (idx == _sel) {
display.setColor(DisplayDriver::LIGHT);
display.fillRect(PM_BX + 1, py - 1, PM_BW - 2, PM_ITEM_H);
display.setColor(DisplayDriver::DARK);
} else {
display.setColor(DisplayDriver::LIGHT);
}
display.setCursor(PM_BX + 4, py);
display.print(_items[idx]);
}
display.setColor(DisplayDriver::LIGHT);
return 50;
}
Result handleInput(char c) {
if (_count == 0) { active = false; return CANCELLED; }
// Selection only moves here; render() keeps it scrolled into view.
if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : _count - 1; return NONE; }
if (c == KEY_DOWN) { _sel = (_sel < _count - 1) ? _sel + 1 : 0; return NONE; }
if (c == KEY_ENTER) { active = false; return SELECTED; }
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { active = false; return CANCELLED; }
return NONE;
}
int selectedIndex() const { return _sel; }
int count() const { return _count; }
void setSelected(int i) { if (i >= 0 && i < _count) _sel = i; }
};