mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-27 13:16:39 +00:00
Continuing the consistency review: sweep for the same three defect shapes
elsewhere in ui-new/ (own read pass plus two parallel research agents),
verified against source before acting.
Three destructive actions fired on a single Enter with no way back, unlike
contact-delete's existing confirm-defaulted-to-Cancel popup: Trail's "Reset
trail" (wipes the whole recorded route, no undo short of a prior manual
Save -- reuses Trail's own multi-level menu machinery, alongside its
GPS-off confirm), Messages' channel Delete, and RadioPresetPicker's saved-
preset delete (shared by Settings > Radio and Tools > Repeater, so one fix
covers both). All three now confirm the same way, defaulting to Cancel.
KeyboardWidget was the one place Hold-Enter still doubled as Cancel: Shift,
Backspace and a Latin letter's accent popup already have real, kept
meanings under a hold, but every other special-row cell (Space, OK/Done,
the {} placeholder) fell through to a bare CANCELLED, closing the keyboard
exactly like the real Cancel key. Now a no-op there too, matching the "only
Back closes it" rule already applied to popups and screens.
MessagesScreen defined the same two label arrays (Notif states, melody
slots) four times over, once per context-menu handler. Hoisted to one
pair of static class members -- constexpr wasn't enough to get the linker
to emit them on this toolchain, so they follow the same declare-in-class/
define-out-of-class shape NearbyScreen::FILTER_LABELS already uses.
Alert text: "Advert sent!"/"Advert failed.." and "Sent!" were the only
toasts anywhere with trailing punctuation; normalized to the plain style
every other confirmation uses. Unpinning from the Favourites Dial reported
the freed slot number from the Messages screens but not from Nodes or the
dial's own tile menu; now consistent everywhere pinning already was.
DiagnosticsScreen's Live/System/Font tab renderers hand-rolled the same
scroll-clamp/loop/indicator skeleton drawList() already bundles; switched
both to drawList (passing the screen's own _scroll as its `sel` too, since
neither tab has a row cursor -- makes drawList's internal clamp a no-op and
leaves clampScroll() as the only thing bounding it, unchanged). Pure
internal tidy, no behavior change.
779 lines
30 KiB
C++
779 lines
30 KiB
C++
#pragma once
|
||
|
||
#include <stdint.h>
|
||
#include <helpers/ui/DisplayDriver.h>
|
||
|
||
// ── Scalable mini-icons ──────────────────────────────────────────────────────
|
||
// Small procedural glyphs (delivery markers, etc.) authored on a 1× pixel grid
|
||
// and scaled to the current font, so they stay legible on large-font layouts
|
||
// (e.g. landscape e-ink renders text at 2×). Distinct from the XBM page icons
|
||
// further down, which are fixed-size full bitmaps drawn via drawXbm().
|
||
//
|
||
// To add a mini-icon:
|
||
// 1. Draw it as ASCII-art rows, one string per row (width ≤ 8): any char
|
||
// other than ' ' or '.' is a filled pixel. packRow() packs each string
|
||
// into one byte at compile time, so the strings never reach flash — the
|
||
// binary holds only the packed bytes, identical to hand-written hex.
|
||
// 2. Wrap the rows in a MINI_ICON(name, width, ...) — width and row count are
|
||
// bundled with the data, so the draw site carries no magic numbers.
|
||
// 3. Draw it with miniIconDraw(display, x, topY, name).
|
||
// Scaling is automatic — no per-display handling needed.
|
||
//
|
||
// packRow() is written as a single-expression recursion so it stays a valid
|
||
// constant expression on any C++ standard the firmware targets (≥ C++11), not
|
||
// just the relaxed C++14 constexpr with loops. It packs up to the string's NUL
|
||
// (capped at 8 cols), so width lives once in MINI_ICON, not in every row.
|
||
constexpr uint8_t packBit(const char* s, int x) {
|
||
return (s[x] && s[x] != ' ' && s[x] != '.') ? (uint8_t)(1u << x) : 0;
|
||
}
|
||
constexpr uint8_t packRow(const char* s, int x = 0) {
|
||
return (!s[x] || x >= 8) ? 0 : (uint8_t)(packBit(s, x) | packRow(s, x + 1));
|
||
}
|
||
|
||
// A mini-icon bundles its pixel data with its dimensions, so call sites can't
|
||
// pass a stale width/height. Built via the MINI_ICON macro below.
|
||
struct MiniIcon { uint8_t w, h; const uint8_t* rows; };
|
||
|
||
// Define a mini-icon: the row count (height) is derived from the initializer,
|
||
// the width is stated once. Emits a packed byte array plus a MiniIcon view.
|
||
#define MINI_ICON(name, width, ...) \
|
||
static constexpr uint8_t name##_rows[] = { __VA_ARGS__ }; \
|
||
static constexpr MiniIcon name = { (uint8_t)(width), \
|
||
(uint8_t)sizeof(name##_rows), name##_rows }
|
||
|
||
// Pixel scale from the font: 1× on an 8px OLED line, 2× on a 16px landscape
|
||
// e-ink line, etc. Bitmaps are authored on the 1× grid.
|
||
inline int miniIconScale(DisplayDriver& d) {
|
||
int s = d.getLineHeight() / 8;
|
||
return s < 1 ? 1 : s;
|
||
}
|
||
|
||
// Draw a w×h (w ≤ 8) bitmap with the current ink colour, scaled by the font and
|
||
// vertically centred in the text line that starts at top_y.
|
||
inline void miniIconDraw(DisplayDriver& d, int x, int top_y,
|
||
const uint8_t* rows, int w, int h) {
|
||
const int s = miniIconScale(d);
|
||
int y = top_y + (d.getLineHeight() - h * s) / 2;
|
||
if (y < top_y) y = top_y;
|
||
for (int r = 0; r < h; r++)
|
||
for (int c = 0; c < w; c++)
|
||
if (rows[r] & (1 << c)) d.fillRect(x + c * s, y + r * s, s, s);
|
||
}
|
||
|
||
// Preferred overload: dimensions travel with the icon — no magic numbers.
|
||
inline void miniIconDraw(DisplayDriver& d, int x, int top_y, const MiniIcon& ic) {
|
||
miniIconDraw(d, x, top_y, ic.rows, ic.w, ic.h);
|
||
}
|
||
|
||
// Draw a mini-icon at an exact top-left (no vertical centring). Used where the
|
||
// caller controls placement, e.g. flush to the top/bottom of a scroll track.
|
||
inline void miniIconDrawTop(DisplayDriver& d, int x, int y, const MiniIcon& ic) {
|
||
const int s = miniIconScale(d);
|
||
for (int r = 0; r < ic.h; r++)
|
||
for (int c = 0; c < ic.w; c++)
|
||
if (ic.rows[r] & (1 << c)) d.fillRect(x + c * s, y + r * s, s, s);
|
||
}
|
||
|
||
// Draw a mini-icon centred on a point (scaled) — used for map markers, which
|
||
// are positioned by their centre rather than a text-line top.
|
||
inline void miniIconDrawCentered(DisplayDriver& d, int cx, int cy, const MiniIcon& ic) {
|
||
const int s = miniIconScale(d);
|
||
miniIconDrawTop(d, cx - (ic.w * s) / 2, cy - (ic.h * s) / 2, ic);
|
||
}
|
||
|
||
// Centre a mini-icon inside the slot [x, 0, box_w, box_h] using the current ink
|
||
// colour (no background). Centres on the box itself, not the text line, so the
|
||
// glyph sits dead-centre regardless of font line height.
|
||
inline void drawSlotIcon(DisplayDriver& d, int x, int box_w, int box_h, const MiniIcon& ic) {
|
||
const int s = miniIconScale(d);
|
||
int ix = x + (box_w - ic.w * s) / 2;
|
||
int iy = (box_h - ic.h * s) / 2;
|
||
if (ix < x) ix = x;
|
||
if (iy < 0) iy = 0;
|
||
miniIconDrawTop(d, ix, iy, ic);
|
||
}
|
||
|
||
// Inverted status indicator: fill the box [x, 0, box_w, box_h] LIGHT, draw the
|
||
// glyph DARK and centred, then restore ink to LIGHT.
|
||
inline void drawBoxedIcon(DisplayDriver& d, int x, int box_w, int box_h, const MiniIcon& ic) {
|
||
d.setColor(DisplayDriver::LIGHT);
|
||
d.fillRect(x, 0, box_w, box_h);
|
||
d.setColor(DisplayDriver::DARK);
|
||
drawSlotIcon(d, x, box_w, box_h, ic);
|
||
d.setColor(DisplayDriver::LIGHT);
|
||
}
|
||
|
||
// Horizontal row of `count` square dots (scaled, vertically centred). Used by
|
||
// the "awaiting ACK" marker, where the dot count = number of send attempts.
|
||
inline void miniIconDotRow(DisplayDriver& d, int x, int top_y, int count) {
|
||
const int s = miniIconScale(d);
|
||
const int dot = 2 * s, pitch = 3 * s; // 2px dot + 1px gap, scaled
|
||
int y = top_y + (d.getLineHeight() - dot) / 2;
|
||
if (y < top_y) y = top_y;
|
||
for (int i = 0; i < count; i++) d.fillRect(x + i * pitch, y, dot, dot);
|
||
}
|
||
|
||
// Mini-icon bitmaps (authored on the 1× grid as ASCII-art; see packRow above).
|
||
MINI_ICON(ICON_CHECK, 5, // ✓
|
||
packRow("....#"),
|
||
packRow("...#."),
|
||
packRow("#.#.."),
|
||
packRow(".#..."));
|
||
MINI_ICON(ICON_CROSS, 4, // ✗
|
||
packRow("#..#"),
|
||
packRow(".##."),
|
||
packRow(".##."),
|
||
packRow("#..#"));
|
||
|
||
// Tiny 3×5 digits — for a small count that needs to sit in an icon-sized slot
|
||
// (e.g. next to ICON_CHECK) where the normal font is too tall to fit. See
|
||
// miniIconDrawNumber/miniIconNumberWidth below.
|
||
MINI_ICON(ICON_DIGIT_0, 3,
|
||
packRow("###"),
|
||
packRow("#.#"),
|
||
packRow("#.#"),
|
||
packRow("#.#"),
|
||
packRow("###"));
|
||
MINI_ICON(ICON_DIGIT_1, 3,
|
||
packRow(".#."),
|
||
packRow("##."),
|
||
packRow(".#."),
|
||
packRow(".#."),
|
||
packRow("###"));
|
||
MINI_ICON(ICON_DIGIT_2, 3,
|
||
packRow("###"),
|
||
packRow("..#"),
|
||
packRow("###"),
|
||
packRow("#.."),
|
||
packRow("###"));
|
||
MINI_ICON(ICON_DIGIT_3, 3,
|
||
packRow("###"),
|
||
packRow("..#"),
|
||
packRow("###"),
|
||
packRow("..#"),
|
||
packRow("###"));
|
||
MINI_ICON(ICON_DIGIT_4, 3,
|
||
packRow("#.#"),
|
||
packRow("#.#"),
|
||
packRow("###"),
|
||
packRow("..#"),
|
||
packRow("..#"));
|
||
MINI_ICON(ICON_DIGIT_5, 3,
|
||
packRow("###"),
|
||
packRow("#.."),
|
||
packRow("###"),
|
||
packRow("..#"),
|
||
packRow("###"));
|
||
MINI_ICON(ICON_DIGIT_6, 3,
|
||
packRow("###"),
|
||
packRow("#.."),
|
||
packRow("###"),
|
||
packRow("#.#"),
|
||
packRow("###"));
|
||
MINI_ICON(ICON_DIGIT_7, 3,
|
||
packRow("###"),
|
||
packRow("..#"),
|
||
packRow("..#"),
|
||
packRow("..#"),
|
||
packRow("..#"));
|
||
MINI_ICON(ICON_DIGIT_8, 3,
|
||
packRow("###"),
|
||
packRow("#.#"),
|
||
packRow("###"),
|
||
packRow("#.#"),
|
||
packRow("###"));
|
||
MINI_ICON(ICON_DIGIT_9, 3,
|
||
packRow("###"),
|
||
packRow("#.#"),
|
||
packRow("###"),
|
||
packRow("..#"),
|
||
packRow("###"));
|
||
|
||
static constexpr const MiniIcon* MINI_ICON_DIGITS[10] = {
|
||
&ICON_DIGIT_0, &ICON_DIGIT_1, &ICON_DIGIT_2, &ICON_DIGIT_3, &ICON_DIGIT_4,
|
||
&ICON_DIGIT_5, &ICON_DIGIT_6, &ICON_DIGIT_7, &ICON_DIGIT_8, &ICON_DIGIT_9,
|
||
};
|
||
|
||
// Width a count would occupy via miniIconDrawNumber (digit width + 1px gap
|
||
// between digits, scaled) — needed up front to size a header around it.
|
||
// Clamped to 2 digits (0-99): callers showing a repeater/echo count never see
|
||
// more than MAX_HIST_PATH_BYTES distinct hashes anyway (16 max).
|
||
inline int miniIconNumberWidth(DisplayDriver& d, int n) {
|
||
const int s = miniIconScale(d);
|
||
int digits = (n >= 10) ? 2 : 1;
|
||
return digits * 3 * s + (digits - 1) * s;
|
||
}
|
||
|
||
// Draws `n` (clamped to 0-99) as a left-to-right run of tiny digit icons —
|
||
// e.g. a repeater/echo count too small a slot for the normal font to fit
|
||
// legibly. Vertically centred in the text line the same way miniIconDraw is.
|
||
inline void miniIconDrawNumber(DisplayDriver& d, int x, int top_y, int n) {
|
||
const int s = miniIconScale(d);
|
||
if (n < 0) n = 0;
|
||
if (n > 99) n = 99;
|
||
if (n >= 10) { miniIconDraw(d, x, top_y, *MINI_ICON_DIGITS[n / 10]); x += 3 * s + s; }
|
||
miniIconDraw(d, x, top_y, *MINI_ICON_DIGITS[n % 10]);
|
||
}
|
||
|
||
// Top-bar status glyphs (replace the single-letter M / B / A indicators).
|
||
MINI_ICON(ICON_MUTE, 6, // speaker + cross (sound off)
|
||
packRow("..#..."),
|
||
packRow(".##..."),
|
||
packRow("####.#"),
|
||
packRow("###.#."),
|
||
packRow("####.#"),
|
||
packRow(".##..."),
|
||
packRow("..#..."));
|
||
MINI_ICON(ICON_BLUETOOTH, 5, // ᛒ bluetooth rune
|
||
packRow("..#.."),
|
||
packRow("..##."),
|
||
packRow("#.#.#"),
|
||
packRow(".###."),
|
||
packRow("#.#.#"),
|
||
packRow("..##."),
|
||
packRow("..#.."));
|
||
MINI_ICON(ICON_ADVERT, 6, // broadcast mast + radiating waves (auto-advert)
|
||
packRow("#....#"),
|
||
packRow(".#..#."),
|
||
packRow("..##.."),
|
||
packRow("..##.."),
|
||
packRow(".####."),
|
||
packRow(".####."));
|
||
|
||
MINI_ICON(ICON_ALARM, 5, // bell — an alarm is armed
|
||
packRow("..#.."),
|
||
packRow(".###."),
|
||
packRow(".###."),
|
||
packRow(".###."),
|
||
packRow("#####"),
|
||
packRow("..#.."));
|
||
|
||
MINI_ICON(ICON_TRAIL, 6, // map pin / location marker (GPS trail logging)
|
||
packRow(".####."),
|
||
packRow("######"),
|
||
packRow("##..##"),
|
||
packRow("######"),
|
||
packRow(".####."),
|
||
packRow("..##.."));
|
||
|
||
MINI_ICON(ICON_REPEATER, 6, // » double chevron — relaying/forwarding (repeater active)
|
||
packRow("#..#.."),
|
||
packRow(".#..#."),
|
||
packRow("..#..#"),
|
||
packRow(".#..#."),
|
||
packRow("#..#.."));
|
||
|
||
MINI_ICON(ICON_GPS, 5, // reticle — GPS fix status (boxed = fix, plain = searching)
|
||
packRow(".###."),
|
||
packRow("#...#"),
|
||
packRow("#.#.#"),
|
||
packRow("#...#"),
|
||
packRow(".###."));
|
||
|
||
// Tools-menu glyphs (auto-reply bot, ringtone editor, diagnostics, system).
|
||
MINI_ICON(ICON_BOT, 5, // robot head: antenna + eyes + grille (auto-reply bot)
|
||
packRow("..#.."),
|
||
packRow("#####"),
|
||
packRow("#.#.#"),
|
||
packRow("#####"),
|
||
packRow("#.#.#"),
|
||
packRow("#####"));
|
||
MINI_ICON(ICON_NOTE, 5, // ♪ quaver — ringtone editor
|
||
packRow("...##"),
|
||
packRow("...##"),
|
||
packRow("...#."),
|
||
packRow("...#."),
|
||
packRow("...#."),
|
||
packRow("####."),
|
||
packRow("####."));
|
||
MINI_ICON(ICON_CHART, 5, // ascending bars — diagnostics / stats
|
||
packRow("....#"),
|
||
packRow("..#.#"),
|
||
packRow("..#.#"),
|
||
packRow("#.#.#"),
|
||
packRow("#.#.#"),
|
||
packRow("#####"));
|
||
MINI_ICON(ICON_GEAR, 7, // ⚙ cog with hub hole — system
|
||
packRow("..#.#.."),
|
||
packRow(".#####."),
|
||
packRow("#######"),
|
||
packRow(".##.##."),
|
||
packRow("#######"),
|
||
packRow(".#####."),
|
||
packRow("..#.#.."));
|
||
MINI_ICON(ICON_KEY, 5, // padlock — remote admin (privileged/password-gated access)
|
||
packRow(".###."),
|
||
packRow("#...#"),
|
||
packRow("#...#"),
|
||
packRow("#####"),
|
||
packRow("##.##"),
|
||
packRow("#####"));
|
||
MINI_ICON(ICON_PINS, 5, // 3-pin header — GPIO
|
||
packRow("#.#.#"),
|
||
packRow("#.#.#"),
|
||
packRow("#####"));
|
||
|
||
// Home-carousel page glyphs — a uniform 5x5 set, deliberately smaller than the
|
||
// menu/status icons above, used in place of the page-indicator dots. One per
|
||
// HomePage; see UITask HomeScreen::pageIcon().
|
||
MINI_ICON(ICON_PG_CLOCK, 5, // clock face + hands
|
||
packRow(".###."),
|
||
packRow("#.#.#"),
|
||
packRow("#.###"),
|
||
packRow("#...#"),
|
||
packRow(".###."));
|
||
MINI_ICON(ICON_PG_STAR, 5, // favourites
|
||
packRow("..#.."),
|
||
packRow("#####"),
|
||
packRow(".###."),
|
||
packRow("##.##"),
|
||
packRow("#...#"));
|
||
MINI_ICON(ICON_PG_RECENT, 5, // stacked lines — recent list
|
||
packRow("#####"),
|
||
packRow("....."),
|
||
packRow("#####"),
|
||
packRow("....."),
|
||
packRow("#####"));
|
||
MINI_ICON(ICON_PG_RADIO, 5, // antenna tower — radio params
|
||
packRow("..#.."),
|
||
packRow(".###."),
|
||
packRow("..#.."),
|
||
packRow(".#.#."),
|
||
packRow("#...#"));
|
||
MINI_ICON(ICON_PG_BT, 5, // bluetooth (compact)
|
||
packRow("..#.."),
|
||
packRow("#.##."),
|
||
packRow(".###."),
|
||
packRow("#.##."),
|
||
packRow("..#.."));
|
||
MINI_ICON(ICON_PG_ADVERT, 5, // mast + radiating waves — advert
|
||
packRow("#...#"),
|
||
packRow(".#.#."),
|
||
packRow("..#.."),
|
||
packRow("..#.."),
|
||
packRow("..#.."));
|
||
MINI_ICON(ICON_PG_GPS, 5, // location pin — GPS
|
||
packRow(".###."),
|
||
packRow("#.#.#"),
|
||
packRow("#.#.#"),
|
||
packRow(".#.#."),
|
||
packRow("..#.."));
|
||
MINI_ICON(ICON_PG_SENSORS, 5, // thermometer/gauge — sensors
|
||
packRow("..#.."),
|
||
packRow(".#.#."),
|
||
packRow(".#.#."),
|
||
packRow(".###."),
|
||
packRow(".###."));
|
||
MINI_ICON(ICON_PG_SETTINGS, 5, // small cog — settings
|
||
packRow(".#.#."),
|
||
packRow("#####"),
|
||
packRow(".#.#."),
|
||
packRow("#####"),
|
||
packRow(".#.#."));
|
||
MINI_ICON(ICON_PG_MAP, 5, // folded map — map page
|
||
packRow("#####"),
|
||
packRow("#.#.#"),
|
||
packRow("#.#.#"),
|
||
packRow("#.#.#"),
|
||
packRow("#####"));
|
||
MINI_ICON(ICON_PG_TOOLS, 5, // wrench (open jaw + handle) — tools
|
||
packRow(".#.#."),
|
||
packRow(".###."),
|
||
packRow("..#.."),
|
||
packRow("..#.."),
|
||
packRow("..#.."));
|
||
MINI_ICON(ICON_PG_MSG, 5, // speech bubble — quick messages
|
||
packRow("#####"),
|
||
packRow("#...#"),
|
||
packRow("#...#"),
|
||
packRow("#####"),
|
||
packRow(".#..."));
|
||
MINI_ICON(ICON_PG_POWER, 5, // power symbol — shutdown
|
||
packRow("..#.."),
|
||
packRow("#.#.#"),
|
||
packRow("#...#"),
|
||
packRow("#...#"),
|
||
packRow(".###."));
|
||
|
||
// Trail-map markers — centred on a point (see miniIconDrawCentered) rather
|
||
// than anchored to a text line.
|
||
MINI_ICON(ICON_MAP_DOT, 3, // ● filled trail point
|
||
packRow("###"),
|
||
packRow("###"),
|
||
packRow("###"));
|
||
MINI_ICON(ICON_MAP_RING, 3, // ○ hollow ring — marks a new trail segment start
|
||
packRow("###"),
|
||
packRow("#.#"),
|
||
packRow("###"));
|
||
MINI_ICON(ICON_MAP_WAYPOINT, 5, // ◇ hollow diamond — saved waypoint
|
||
packRow("..#.."),
|
||
packRow(".#.#."),
|
||
packRow("#...#"),
|
||
packRow(".#.#."),
|
||
packRow("..#.."));
|
||
MINI_ICON(ICON_MAP_START, 5, // + trail start marker
|
||
packRow("..#.."),
|
||
packRow("..#.."),
|
||
packRow("#####"),
|
||
packRow("..#.."),
|
||
packRow("..#.."));
|
||
MINI_ICON(ICON_MAP_CURRENT, 5, // ✕ live position / last trail point
|
||
packRow("#...#"),
|
||
packRow(".#.#."),
|
||
packRow("..#.."),
|
||
packRow(".#.#."),
|
||
packRow("#...#"));
|
||
MINI_ICON(ICON_MAP_CONTACT, 5, // ◆ filled diamond — a live-tracked contact ([LOC] share)
|
||
packRow("..#.."),
|
||
packRow(".###."),
|
||
packRow("#####"),
|
||
packRow(".###."),
|
||
packRow("..#.."));
|
||
MINI_ICON(ICON_MAP_NORTH, 5, // "N" with a peaked roof — compass north marker
|
||
packRow("..#.."),
|
||
packRow(".###."),
|
||
packRow("#...#"),
|
||
packRow("##..#"),
|
||
packRow("#.#.#"),
|
||
packRow("#..##"),
|
||
packRow("#...#"));
|
||
MINI_ICON(ICON_MAP_ARROW, 5, // → distance-to-nearest-tracked-contact indicator
|
||
packRow("..#.."),
|
||
packRow("...#."),
|
||
packRow("#####"),
|
||
packRow("...#."),
|
||
packRow("..#.."));
|
||
MINI_ICON(ICON_MAP_TARGET, 5, // ⚑ flag on a pole — the active Locator/Nav target
|
||
packRow("####."),
|
||
packRow("#..#."),
|
||
packRow("####."),
|
||
packRow("#...."),
|
||
packRow("#...."));
|
||
|
||
// Keyboard special-key glyphs.
|
||
MINI_ICON(ICON_SHIFT, 7, // ⇧ caps
|
||
packRow("...#..."),
|
||
packRow("..###.."),
|
||
packRow(".#####."),
|
||
packRow("#######"),
|
||
packRow("..###.."),
|
||
packRow("..###.."),
|
||
packRow("..###.."));
|
||
MINI_ICON(ICON_BACKSPACE, 8, // ⌫ delete-left (× knocked out of the arrow body)
|
||
packRow("...#####"),
|
||
packRow("..######"),
|
||
packRow(".###.#.#"),
|
||
packRow("#####.##"),
|
||
packRow(".###.#.#"),
|
||
packRow("..######"),
|
||
packRow("...#####"));
|
||
// Space ⎵ is wider than the 8-px mini-icon limit, so it is two halves drawn
|
||
// side by side (offset by ICON_SPACE_L.w * scale): ticks at both far ends + bar.
|
||
MINI_ICON(ICON_SPACE_L, 8,
|
||
packRow("#......."),
|
||
packRow("#......."),
|
||
packRow("#......."),
|
||
packRow("########"));
|
||
MINI_ICON(ICON_SPACE_R, 8,
|
||
packRow(".......#"),
|
||
packRow(".......#"),
|
||
packRow(".......#"),
|
||
packRow("########"));
|
||
|
||
// Scroll-indicator caps — small up/down triangles (authored on the 1× grid).
|
||
MINI_ICON(ICON_SCROLL_UP, 5, // ▲
|
||
packRow("..#.."),
|
||
packRow(".###."),
|
||
packRow("#####"));
|
||
MINI_ICON(ICON_SCROLL_DOWN, 5, // ▼
|
||
packRow("#####"),
|
||
packRow(".###."),
|
||
packRow("..#.."));
|
||
|
||
// Width of the right-edge column drawScrollIndicator occupies, or 0 when the
|
||
// list fits and no indicator is drawn. Subtract from a row's content width so
|
||
// text never runs under the scrollbar. Mirrors the column math below (5*scale
|
||
// triangle + 1px gap).
|
||
inline int scrollIndicatorColWidth(DisplayDriver& d) {
|
||
return 5 * miniIconScale(d) + 1; // triangle (5*scale) + 1px gap
|
||
}
|
||
inline int scrollIndicatorReserve(DisplayDriver& d, int total, int visible) {
|
||
return (total > visible) ? scrollIndicatorColWidth(d) : 0;
|
||
}
|
||
|
||
// Right-edge scroll indicator: a proportional track + thumb topped/tailed with
|
||
// up/down triangle mini-icon caps. Drop-in replacement for
|
||
// DisplayDriver::drawScrollArrows that also shows how much of the list is on
|
||
// screen and where you are within it. Everything lives in a ~5px column at the
|
||
// right edge and scales with the font (mini-icon scale).
|
||
// top_y : y of the first visible row (top of the viewport)
|
||
// track_h : pixel height of the viewport (e.g. visible_rows * row_pitch)
|
||
//
|
||
// Core takes the proportions in arbitrary units. For uniform-height lists pass
|
||
// item counts (see the item wrapper below). For variable-height lists (e.g. the
|
||
// portrait DM history, where each message box wraps to a different height) pass
|
||
// pixel sums — total_px = Σ all box heights, view_px = pixels currently on
|
||
// screen, scroll_px = pixels above the first visible box — so the thumb size and
|
||
// position track real content extent instead of jumping as box counts fluctuate.
|
||
// total_px : total extent of the whole list (items or pixels)
|
||
// view_px : extent currently visible
|
||
// scroll_px : extent scrolled past (offset of the first visible item)
|
||
// right_x : column origin's right edge, in screen pixels (the indicator
|
||
// occupies [right_x - col, right_x)). Full-screen lists pass
|
||
// d.width(); a narrower container (e.g. a centred popup) passes its
|
||
// own right edge so the indicator lands inside the box, not the screen.
|
||
inline void drawScrollIndicatorPx(DisplayDriver& d, int right_x, int top_y, int track_h,
|
||
long total_px, long view_px, long scroll_px) {
|
||
if (total_px <= view_px || track_h <= 0) return; // whole list fits — no indicator
|
||
const long span = total_px - view_px; // > 0 here
|
||
if (scroll_px < 0) scroll_px = 0;
|
||
if (scroll_px > span) scroll_px = span;
|
||
|
||
const int s = miniIconScale(d);
|
||
const int col = 5 * s; // triangle / column width
|
||
const int th = 3 * s; // triangle height
|
||
const int x = right_x - col; // indicator column origin
|
||
|
||
// Caps are static end-markers, always drawn flush at the very top / bottom of
|
||
// the track; the thumb travels in the fixed band between them. (They mark the
|
||
// track ends, not "more above/below", so they never vanish at the extremes.)
|
||
const int cap = th + 2; // triangle height + 2px gap
|
||
const int band_t = top_y + cap;
|
||
const int band_b = top_y + track_h - cap;
|
||
int band = band_b - band_t;
|
||
if (band < s) band = s;
|
||
|
||
const int bar_w = 3 * s; // odd width → centres exactly in the 5*s column
|
||
const int bar_x = x + (col - bar_w) / 2;
|
||
|
||
int thumb_h = (int)((long)band * view_px / total_px);
|
||
if (thumb_h < th) thumb_h = th;
|
||
if (thumb_h > band) thumb_h = band;
|
||
const int thumb_y = band_t + (int)((long)(band - thumb_h) * scroll_px / span);
|
||
|
||
// No halo: every caller already keeps its selection bar clear of this column
|
||
// (reserve/_reserve), so the markers never need to fight a LIGHT background
|
||
// for contrast — and a halo on the bottom arrow had no symmetric clip like
|
||
// the top one, so it bled 1px into the container's bottom border.
|
||
d.setColor(DisplayDriver::LIGHT);
|
||
d.fillRect(bar_x, thumb_y, bar_w, thumb_h);
|
||
|
||
miniIconDrawTop(d, x, top_y, ICON_SCROLL_UP);
|
||
miniIconDrawTop(d, x, top_y + track_h - th, ICON_SCROLL_DOWN);
|
||
}
|
||
|
||
// Convenience overload anchored to the screen's right edge — the common case
|
||
// for full-width lists.
|
||
inline void drawScrollIndicatorPx(DisplayDriver& d, int top_y, int track_h,
|
||
long total_px, long view_px, long scroll_px) {
|
||
drawScrollIndicatorPx(d, d.width(), top_y, track_h, total_px, view_px, scroll_px);
|
||
}
|
||
|
||
// Uniform-height wrapper: one item = one unit. Drop-in replacement for
|
||
// DisplayDriver::drawScrollArrows.
|
||
// total : total number of items
|
||
// visible : items shown at once
|
||
// first : index of the first visible item (scroll offset)
|
||
inline void drawScrollIndicator(DisplayDriver& d, int top_y, int track_h,
|
||
int total, int visible, int first) {
|
||
drawScrollIndicatorPx(d, top_y, track_h, total, visible, first);
|
||
}
|
||
|
||
// right_x variant — see drawScrollIndicatorPx's right_x for why a narrower
|
||
// container (e.g. a popup) needs this instead of the screen-edge default.
|
||
inline void drawScrollIndicator(DisplayDriver& d, int right_x, int top_y, int track_h,
|
||
int total, int visible, int first) {
|
||
drawScrollIndicatorPx(d, right_x, top_y, track_h, total, visible, first);
|
||
}
|
||
|
||
// 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 <class RenderRow>
|
||
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;
|
||
}
|
||
|
||
// Canonical selection bar for a drawList() row: spans the row width minus the
|
||
// scroll-indicator `reserve`, one pixel short of the row height, anchored one
|
||
// pixel above `y` (the row's text baseline-top). Call as the first line of a
|
||
// row callback, then draw content over it. Captures the geometry every list row
|
||
// repeated by hand; rows that intentionally differ (full-width, custom height)
|
||
// still call display.drawSelectionRow() directly.
|
||
inline void drawRowSelection(DisplayDriver& d, int y, bool sel, int reserve) {
|
||
d.drawSelectionRow(0, y - 1, d.width() - reserve, d.lineStep() - 1, sel);
|
||
}
|
||
|
||
// ── 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/…)
|
||
// stay hand-encoded; reach for this when adding a *new* big icon so it's
|
||
// readable in source. Drawn at 1× (page icons aren't font-scaled).
|
||
//
|
||
// To add one:
|
||
// BIG_ICON(MY_ICON, 16,
|
||
// packRow32("......####......"),
|
||
// packRow32(".....######....."),
|
||
// ...);
|
||
// bigIconDraw(display, x, y, MY_ICON);
|
||
constexpr uint32_t packBit32(const char* s, int x) {
|
||
return (s[x] && s[x] != ' ' && s[x] != '.') ? (1u << x) : 0u;
|
||
}
|
||
constexpr uint32_t packRow32(const char* s, int x = 0) {
|
||
return (!s[x] || x >= 32) ? 0u : (packBit32(s, x) | packRow32(s, x + 1));
|
||
}
|
||
|
||
struct BigIcon { uint8_t w, h; const uint32_t* rows; };
|
||
|
||
#define BIG_ICON(name, width, ...) \
|
||
static constexpr uint32_t name##_rows[] = { __VA_ARGS__ }; \
|
||
static constexpr BigIcon name = { \
|
||
(uint8_t)(width), \
|
||
(uint8_t)(sizeof(name##_rows) / sizeof(uint32_t)), name##_rows }
|
||
|
||
// Draw a packed big icon at 1× with the current ink colour, top-left at (x, y).
|
||
inline void bigIconDraw(DisplayDriver& d, int x, int y, const BigIcon& ic) {
|
||
for (int r = 0; r < ic.h; r++)
|
||
for (int c = 0; c < ic.w; c++)
|
||
if (ic.rows[r] & (1u << c)) d.fillRect(x + c, y + r, 1, 1);
|
||
}
|
||
|
||
// 'meshcore', 128x13px
|
||
static const uint8_t meshcore_logo [] = {
|
||
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
|
||
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
|
||
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
|
||
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
|
||
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
|
||
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
|
||
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
|
||
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
|
||
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
|
||
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
|
||
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
|
||
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
|
||
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
|
||
};
|
||
|
||
static const uint8_t bluetooth_on[] = {
|
||
0x00, 0x00, 0x00, 0x00,
|
||
0x00, 0x00, 0x00, 0x00,
|
||
0x00, 0x00, 0x00, 0x00,
|
||
0x00, 0x30, 0x00, 0x00,
|
||
0x00, 0x3C, 0x00, 0x00,
|
||
0x00, 0x3E, 0x00, 0x00,
|
||
0x00, 0x3F, 0x80, 0x00,
|
||
0x00, 0x3F, 0xC0, 0x00,
|
||
0x00, 0x3B, 0xE0, 0x00,
|
||
0x30, 0x38, 0xF8, 0x00,
|
||
0x3C, 0x38, 0x7C, 0x00,
|
||
0x3E, 0x38, 0x7C, 0x00,
|
||
0x1F, 0xB8, 0xF8, 0x70,
|
||
0x07, 0xF9, 0xF0, 0x78,
|
||
0x03, 0xFF, 0xC0, 0x78,
|
||
0x00, 0xFF, 0x80, 0x3C,
|
||
0x00, 0x7F, 0x07, 0x1C,
|
||
0x00, 0x7E, 0x07, 0x1C,
|
||
0x03, 0xFF, 0x82, 0x1C,
|
||
0x03, 0xFF, 0xC0, 0x78,
|
||
0x07, 0xFB, 0xE0, 0x78,
|
||
0x0F, 0xB8, 0xF8, 0x70,
|
||
0x3E, 0x38, 0x7C, 0x00,
|
||
0x3C, 0x38, 0x7C, 0x00,
|
||
0x38, 0x38, 0xF8, 0x00,
|
||
0x00, 0x39, 0xF0, 0x00,
|
||
0x00, 0x3F, 0xC0, 0x00,
|
||
0x00, 0x3F, 0x80, 0x00,
|
||
0x00, 0x3E, 0x00, 0x00,
|
||
0x00, 0x3C, 0x00, 0x00,
|
||
0x00, 0x38, 0x00, 0x00,
|
||
0x00, 0x00, 0x00, 0x00,
|
||
};
|
||
|
||
static const uint8_t bluetooth_off[] = {
|
||
0x00, 0x00, 0x00, 0x00,
|
||
0x00, 0x00, 0x00, 0x00,
|
||
0x00, 0x03, 0x80, 0x00,
|
||
0x00, 0x03, 0xC0, 0x00,
|
||
0x00, 0x03, 0xE0, 0x00,
|
||
0x38, 0x03, 0xF8, 0x00,
|
||
0x3C, 0x03, 0xFC, 0x00,
|
||
0x3E, 0x03, 0xBF, 0x00,
|
||
0x0F, 0x83, 0x8F, 0x80,
|
||
0x07, 0xC3, 0x87, 0xC0,
|
||
0x03, 0xF0, 0x03, 0xC0,
|
||
0x00, 0xF8, 0x0F, 0x80,
|
||
0x00, 0x7C, 0x0F, 0x00,
|
||
0x00, 0x1F, 0x0E, 0x00,
|
||
0x00, 0x0F, 0x80, 0x00,
|
||
0x00, 0x07, 0xE0, 0x00,
|
||
0x00, 0x07, 0xF0, 0x00,
|
||
0x00, 0x0F, 0xF8, 0x00,
|
||
0x00, 0x3F, 0xBE, 0x00,
|
||
0x00, 0x7F, 0x9F, 0x00,
|
||
0x00, 0xFB, 0x8F, 0xC0,
|
||
0x03, 0xE3, 0x83, 0xE0,
|
||
0x03, 0xC3, 0x87, 0xF0,
|
||
0x03, 0x83, 0x8F, 0xFC,
|
||
0x00, 0x03, 0xBF, 0x3C,
|
||
0x00, 0x03, 0xFC, 0x1C,
|
||
0x00, 0x03, 0xF8, 0x00,
|
||
0x00, 0x03, 0xE0, 0x00,
|
||
0x00, 0x03, 0xC0, 0x00,
|
||
0x00, 0x03, 0x80, 0x00,
|
||
0x00, 0x00, 0x00, 0x00,
|
||
0x00, 0x00, 0x00, 0x00,
|
||
};
|
||
|
||
static const uint8_t power_icon[] = {
|
||
0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00,
|
||
0x00, 0x33, 0xCC, 0x00, 0x00, 0xF3, 0xCF, 0x00, 0x01, 0xF3, 0xCF, 0x80,
|
||
0x03, 0xF3, 0xCF, 0xC0, 0x07, 0xF3, 0xCF, 0xE0, 0x0F, 0xE3, 0xC7, 0xF0,
|
||
0x1F, 0xC3, 0xC3, 0xF8, 0x1F, 0x83, 0xC1, 0xF8, 0x3F, 0x03, 0xC0, 0xFC,
|
||
0x3E, 0x03, 0xC0, 0x7C, 0x3E, 0x03, 0xC0, 0x7C, 0x7E, 0x01, 0x80, 0x7E,
|
||
0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E,
|
||
0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0x7C,
|
||
0x3E, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0xFC, 0x1F, 0x80, 0x01, 0xF8,
|
||
0x1F, 0xC0, 0x03, 0xF8, 0x0F, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xF0,
|
||
0x07, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x00,
|
||
0x00, 0x3F, 0xFC, 0x00, 0x00, 0x0F, 0xF0, 0x00,
|
||
};
|
||
|
||
static const uint8_t advert_icon[] = {
|
||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x30,
|
||
0x1C, 0x00, 0x00, 0x38, 0x18, 0x00, 0x00, 0x18, 0x30, 0x00, 0x00, 0x0C,
|
||
0x30, 0x60, 0x06, 0x0C, 0x60, 0xE0, 0x07, 0x06, 0x61, 0xC0, 0x03, 0x86,
|
||
0xE1, 0x81, 0x81, 0x87, 0xC3, 0x07, 0xE0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3,
|
||
0xC3, 0x0F, 0xF0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3,
|
||
0xC3, 0x07, 0xE0, 0xC3, 0xC1, 0x83, 0xC1, 0x83, 0x61, 0x80, 0x01, 0x86,
|
||
0x60, 0xC0, 0x03, 0x06, 0x70, 0xE0, 0x07, 0x0E, 0x30, 0x40, 0x02, 0x0C,
|
||
0x38, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x30,
|
||
0x04, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||
};
|
||
|
||
// Favourite marker for a list row, on every screen that lists something
|
||
// starrable. Reuses the Favourites page's own icon so the two read as one idea.
|
||
// Width includes the gap that separates it from the text to its left.
|
||
inline int favStarWidth(DisplayDriver& d) {
|
||
return ICON_PG_STAR.w * miniIconScale(d) + 2;
|
||
}
|
||
inline void drawFavStar(DisplayDriver& d, int x, int top_y) {
|
||
miniIconDraw(d, x, top_y, ICON_PG_STAR);
|
||
}
|