From cea102c15275e7773f48fb58bd7849dff1f07c95 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Tue, 6 Oct 2026 13:44:11 +0200 Subject: [PATCH] feat(eink): 9px icon set, 5x10 hop digits and roomier bubbles on the 4.2" Co-Authored-By: Claude Opus 5.5 --- .../companion_radio/ui-new/KeyboardWidget.h | 27 +- .../companion_radio/ui-new/MessagesScreen.h | 13 +- examples/companion_radio/ui-new/icons.h | 687 +++++++++++++++++- 3 files changed, 681 insertions(+), 46 deletions(-) diff --git a/examples/companion_radio/ui-new/KeyboardWidget.h b/examples/companion_radio/ui-new/KeyboardWidget.h index 56b03d86..72be5af5 100644 --- a/examples/companion_radio/ui-new/KeyboardWidget.h +++ b/examples/companion_radio/ui-new/KeyboardWidget.h @@ -569,7 +569,9 @@ struct KeyboardWidget { // lines: cursor_mode's own 3-line hint (drawn in this same region, // regardless of Compact, for a physical-button user) already relies on // that floor and would otherwise get clipped. - const int kb_h = compact_ui ? (KB_T9_ROWS + 1) * lh : (rows + 1) * lh; + // A tall screen (the 4.2" panel: 20 lines) gives the keys half a line more each. + const int key_lh = display.height() >= 18 * lh ? lh * 3 / 2 : lh; + const int kb_h = compact_ui ? (KB_T9_ROWS + 1) * lh : (rows + 1) * key_lh; const int preview_h = display.height() - kb_h - display.sepH(); const int prev_lines = (preview_h / lh) > 1 ? (preview_h / lh) : 1; const int sep_y = prev_lines * lh; @@ -577,6 +579,11 @@ struct KeyboardWidget { const int cell_h = (display.height() - chars_y) / (rows + 1); const int spec_y = chars_y + rows * cell_h; const int spec_w = display.width() / KB_SPECIAL; + const int key_ty = key_lh > lh ? (cell_h - lh) / 2 : 0; // a key's text, centred in a tall cell + // A special key's icon top: by the text line as ever, by its own height in a tall cell. + auto iconY = [&](const MiniIcon& ic) { + return spec_y + (key_lh > lh ? (cell_h - ic.h * miniIconScale(display)) / 2 : (cell_h - lh) / 2); + }; // Multi-line text preview: the view follows cursor_pos (normally == len, // i.e. the end — so this is identical to the old "always the last line" @@ -706,13 +713,12 @@ struct KeyboardWidget { ch_buf[0] = ch; ch_buf[1] = '\0'; int tw = display.getTextWidth(ch_buf); - display.setCursor(cx + (cell_w - tw) / 2, y); + display.setCursor(cx + (cell_w - tw) / 2, y + key_ty); display.print(ch_buf); } } // special row with backspace, abc kb switch & submit const int psw = display.width() / KB_PIN_SPECIAL; - const int icy = spec_y + (cell_h - lh) / 2; for (int i = 0; i < KB_PIN_SPECIAL; i++) { bool sel = (row == rows && col == i); int sx = i * psw; @@ -721,7 +727,7 @@ struct KeyboardWidget { : (i == 1) ? ICON_KEYBOARD : ICON_CHECK; int ix = sx + (psw - ic.w * s) / 2; - miniIconDraw(display, ix, icy, ic); + miniIconDraw(display, ix, iconY(ic), ic); } return 50; } @@ -763,7 +769,7 @@ struct KeyboardWidget { int cx = c * cell_w; display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel); int tw = display.getTextWidth(label); - display.setCursor(cx + (cell_w - tw) / 2, y); + display.setCursor(cx + (cell_w - tw) / 2, y + key_ty); display.print(label); } } @@ -778,7 +784,7 @@ struct KeyboardWidget { int cx = c * cell_w; display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel); int tw = display.getTextWidth(ch_buf); - display.setCursor(cx + (cell_w - tw) / 2, y); + display.setCursor(cx + (cell_w - tw) / 2, y + key_ty); display.print(ch_buf); } } @@ -786,7 +792,6 @@ struct KeyboardWidget { // special row: caps ⇧ · space ⎵ · delete ⌫ · placeholders {} (text) · OK ✓ const int s = miniIconScale(display); - const int icy = spec_y + (cell_h - lh) / 2; // centre icons within the cell for (int i = 0; i < KB_SPECIAL; i++) { bool sel = (row == rows && col == i); bool active = (i == 0 && caps); @@ -804,19 +809,19 @@ struct KeyboardWidget { lbl = pageIsSymbols(next) ? "#@" : scriptHint(scriptAt(next)); } int tw = display.getTextWidth(lbl); - display.setCursor(sx + (spec_w - tw) / 2, spec_y); + display.setCursor(sx + (spec_w - tw) / 2, spec_y + key_ty); display.print(lbl); } else if (i == 1) { // space ⎵ — two halves side by side int icw = (ICON_SPACE_L.w + ICON_SPACE_R.w) * s; int ix = sx + (spec_w - icw) / 2; - miniIconDraw(display, ix, icy, ICON_SPACE_L); - miniIconDraw(display, ix + ICON_SPACE_L.w * s, icy, ICON_SPACE_R); + miniIconDraw(display, ix, iconY(ICON_SPACE_L), ICON_SPACE_L); + miniIconDraw(display, ix + ICON_SPACE_L.w * s, iconY(ICON_SPACE_R), ICON_SPACE_R); } else { const MiniIcon& ic = (i == 0) ? ICON_SHIFT : (i == 2) ? ICON_BACKSPACE : ICON_CHECK; // i == 5 → OK int ix = sx + (spec_w - ic.w * s) / 2; - miniIconDraw(display, ix, icy, ic); + miniIconDraw(display, ix, iconY(ic), ic); // Underline the ⇧ icon while caps_lock is held. Without it the two // Shift states are indistinguishable -- caps_lock sets caps too, so // the highlight above is identical -- even though they behave diff --git a/examples/companion_radio/ui-new/MessagesScreen.h b/examples/companion_radio/ui-new/MessagesScreen.h index e6ab8fd4..5d717f2c 100644 --- a/examples/companion_radio/ui-new/MessagesScreen.h +++ b/examples/companion_radio/ui-new/MessagesScreen.h @@ -519,7 +519,7 @@ class MessagesScreen : public UIScreen { static int ackGlyphWidth(DisplayDriver& d, AckState s, int sends, int relay_count = 0) { const int sc = miniIconScale(d); switch (s) { - case ACK_PENDING: return sends * 3 * sc; // dot+gap pitch (icons.h), slightly generous + case ACK_PENDING: return miniIconDotRowWidth(d, sends); case ACK_OK: return relay_count > 0 ? miniIconNumberWidth(d, relay_count) : ICON_CHECK.w * sc; case ACK_FAIL: return ICON_CROSS.w * sc; default: return 0; @@ -553,7 +553,7 @@ class MessagesScreen : public UIScreen { static int expandedBoxH(DisplayDriver& d, const char* body, int full_avail) { d.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans)); int nl = FullscreenMsgView::wrapLines(d, s_wrap_trans, bubbleMaxW(d, full_avail) - 6, s_wrap_lines, 8); - return (1 + (nl > 0 ? nl : 1)) * d.getLineHeight() + 1; + return (1 + (nl > 0 ? nl : 1)) * d.getLineHeight() + 3; // 2 px under the descenders } static BubbleBox computeBubbleBox(int full_avail, int max_w, bool outgoing, int header_w, int body_w) { int w = header_w > body_w ? header_w : body_w; @@ -593,7 +593,10 @@ class MessagesScreen : public UIScreen { const int ack_w = mark.width(display); // A little air between a marker and the age, so "12" + "1s" doesn't read as "121s". const int mk_gap = (ack_w > 0 && age_w > 0) ? 2 : 0; - const int header_w = 3 + (sender[0] ? display.getTextWidth(sender) : -3) + ack_w + mk_gap + age_w + 3; + // The marker after the name: a character's gap beside the larger icons, + // whose digits stand as tall as the text and would read as part of it. + const int nm_gap = MINI_ICONS_LARGE ? display.getCharWidth() : 3; + const int header_w = 3 + (sender[0] ? display.getTextWidth(sender) + (ack_w ? nm_gap - 3 : 0) : -3) + ack_w + mk_gap + age_w + 3; int body_w, nl = 0; if (expand) { display.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans)); @@ -614,12 +617,12 @@ class MessagesScreen : public UIScreen { // The name gives way to the marker and the age (a long name is // ellipsized), and the marker sits right after whatever width the name // actually got -- not after its full width, which would run it into the age. - int name_avail = box.w - 6 - age_w - ack_w - mk_gap; + int name_avail = box.w - 6 - age_w - ack_w - mk_gap - (ack_w ? nm_gap - 3 : 0); if (name_avail < display.getCharWidth()) name_avail = display.getCharWidth(); int name_w = sender[0] ? display.getTextWidth(sender) : 0; if (name_w > name_avail) name_w = name_avail; if (sender[0]) display.drawTextEllipsized(box.x + 3, y + 1, name_avail, sender); - int gx = box.x + 3 + (name_w ? name_w + 3 : 0); + int gx = box.x + 3 + (name_w ? name_w + nm_gap : 0); if (mark.kind == BubbleMark::ACK) drawAckGlyph(display, gx, y + 1, mark.status, mark.sends, mark.relay); else if (mark.kind == BubbleMark::HOPS) miniIconDrawNumber(display, gx, y + 1, mark.hops); if (age[0]) { display.setCursor(box.x + box.w - age_w, y + 1); display.print(age); } diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index e247035b..ed022aac 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -10,10 +10,10 @@ // (e.g. landscape e-ink renders text at 2×). // // To add a mini-icon: -// 1. Draw it as ASCII-art rows, one string per row (width ≤ 8): any char +// 1. Draw it as ASCII-art rows, one string per row (width ≤ 16): 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. +// into a 16-bit word at compile time, so the strings never reach flash — +// the binary holds only the packed words, 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). @@ -22,33 +22,34 @@ // 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; +// (capped at 16 cols), so width lives once in MINI_ICON, not in every row. +constexpr uint16_t packBit(const char* s, int x) { + return (s[x] && s[x] != ' ' && s[x] != '.') ? (uint16_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)); +constexpr uint16_t packRow(const char* s, int x = 0) { + return (!s[x] || x >= 16) ? 0 : (uint16_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; }; +struct MiniIcon { uint8_t w, h; const uint16_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. +// the width is stated once. Emits a packed word array plus a MiniIcon view. #define MINI_ICON(name, width, ...) \ - static constexpr uint8_t name##_rows[] = { __VA_ARGS__ }; \ + static constexpr uint16_t name##_rows[] = { __VA_ARGS__ }; \ static constexpr MiniIcon name = { (uint8_t)(width), \ - (uint8_t)sizeof(name##_rows), name##_rows } + (uint8_t)(sizeof(name##_rows) / sizeof(name##_rows[0])), name##_rows } // The landscape e-ink build writes in the 8x13, whose digits stand 9 px: // there the status, page and check glyphs come in a 7 px set instead of 5. +// The 4.2" build's 9x15 (digits 10 px) takes a 9 px set: MINI_ICONS_LARGE 2. #if defined(EINK_LARGE_FONT) && EINK_LARGE_FONT - #define MINI_ICONS_LARGE 1 + #define MINI_ICONS_LARGE EINK_LARGE_FONT #else #define MINI_ICONS_LARGE 0 #endif -static constexpr int PAGE_ICON_PX = MINI_ICONS_LARGE ? 7 : 5; // the page glyphs' box +static constexpr int PAGE_ICON_PX = MINI_ICONS_LARGE == 2 ? 9 : MINI_ICONS_LARGE ? 7 : 5; // the page glyphs' box // 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. @@ -57,10 +58,10 @@ inline int miniIconScale(DisplayDriver& d) { return s < 1 ? 1 : s; } -// Draw a w×h (w ≤ 8) bitmap with the current ink colour, scaled by the font and +// Draw a w×h (w ≤ 16) 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 uint16_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; @@ -104,16 +105,35 @@ inline void drawSlotIcon(DisplayDriver& d, int x, int box_w, int box_h, const Mi // Horizontal row of `count` square dots (scaled, vertically centred). Used by // the "awaiting ACK" marker, where the dot count = number of send attempts. +// The dots are 3 px beside the larger icon sets. +static constexpr int DOT_ROW_PX = MINI_ICONS_LARGE ? 3 : 2; +inline int miniIconDotRowWidth(DisplayDriver& d, int count) { return count * (DOT_ROW_PX + 1) * miniIconScale(d); } 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 + const int dot = DOT_ROW_PX * s, pitch = (DOT_ROW_PX + 1) * s; // 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). -#if MINI_ICONS_LARGE +#if MINI_ICONS_LARGE == 2 +MINI_ICON(ICON_CHECK, 9, // ✓ + packRow("........#"), + packRow(".......#."), + packRow("#.....#.."), + packRow(".#...#..."), + packRow("..#.#...."), + packRow("...#.....")); +MINI_ICON(ICON_CROSS, 7, // ✗ + packRow("#.....#"), + packRow(".#...#."), + packRow("..#.#.."), + packRow("...#..."), + packRow("..#.#.."), + packRow(".#...#."), + packRow("#.....#")); +#elif MINI_ICONS_LARGE MINI_ICON(ICON_CHECK, 7, // ✓ packRow("......#"), packRow(".....#."), @@ -139,9 +159,202 @@ MINI_ICON(ICON_CROSS, 4, // ✗ packRow("#..#")); #endif -// Tiny 3×5 digits — for a small count that needs to sit in an icon-sized slot +// Tiny 3×5 digits (5x7 and 5x10 in the large sets) — 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. +#if MINI_ICONS_LARGE == 2 // 5x10, as tall as the 9x15's own digits +MINI_ICON(ICON_DIGIT_0, 5, + packRow(".###."), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow(".###.")); +MINI_ICON(ICON_DIGIT_1, 5, + packRow("..#.."), + packRow(".##.."), + packRow("#.#.."), + packRow("..#.."), + packRow("..#.."), + packRow("..#.."), + packRow("..#.."), + packRow("..#.."), + packRow("..#.."), + packRow("#####")); +MINI_ICON(ICON_DIGIT_2, 5, + packRow(".###."), + packRow("#...#"), + packRow("....#"), + packRow("....#"), + packRow("...#."), + packRow("..#.."), + packRow(".#..."), + packRow("#...."), + packRow("#...."), + packRow("#####")); +MINI_ICON(ICON_DIGIT_3, 5, + packRow(".###."), + packRow("#...#"), + packRow("....#"), + packRow("....#"), + packRow("..##."), + packRow("....#"), + packRow("....#"), + packRow("....#"), + packRow("#...#"), + packRow(".###.")); +MINI_ICON(ICON_DIGIT_4, 5, + packRow("...#."), + packRow("..##."), + packRow(".#.#."), + packRow("#..#."), + packRow("#..#."), + packRow("#..#."), + packRow("#####"), + packRow("...#."), + packRow("...#."), + packRow("...#.")); +MINI_ICON(ICON_DIGIT_5, 5, + packRow("#####"), + packRow("#...."), + packRow("#...."), + packRow("#...."), + packRow("####."), + packRow("....#"), + packRow("....#"), + packRow("....#"), + packRow("#...#"), + packRow(".###.")); +MINI_ICON(ICON_DIGIT_6, 5, + packRow(".###."), + packRow("#...#"), + packRow("#...."), + packRow("#...."), + packRow("####."), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow(".###.")); +MINI_ICON(ICON_DIGIT_7, 5, + packRow("#####"), + packRow("....#"), + packRow("....#"), + packRow("...#."), + packRow("...#."), + packRow("..#.."), + packRow("..#.."), + packRow("..#.."), + packRow("..#.."), + packRow("..#..")); +MINI_ICON(ICON_DIGIT_8, 5, + packRow(".###."), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow(".###."), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow(".###.")); +MINI_ICON(ICON_DIGIT_9, 5, + packRow(".###."), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow(".####"), + packRow("....#"), + packRow("....#"), + packRow("#...#"), + packRow(".###.")); +#elif MINI_ICONS_LARGE // 5x7 beside the 7 px ones +MINI_ICON(ICON_DIGIT_0, 5, + packRow(".###."), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow(".###.")); +MINI_ICON(ICON_DIGIT_1, 5, + packRow("..#.."), + packRow(".##.."), + packRow("..#.."), + packRow("..#.."), + packRow("..#.."), + packRow("..#.."), + packRow(".###.")); +MINI_ICON(ICON_DIGIT_2, 5, + packRow(".###."), + packRow("#...#"), + packRow("....#"), + packRow("...#."), + packRow("..#.."), + packRow(".#..."), + packRow("#####")); +MINI_ICON(ICON_DIGIT_3, 5, + packRow("#####"), + packRow("...#."), + packRow("..#.."), + packRow("...#."), + packRow("....#"), + packRow("#...#"), + packRow(".###.")); +MINI_ICON(ICON_DIGIT_4, 5, + packRow("...#."), + packRow("..##."), + packRow(".#.#."), + packRow("#..#."), + packRow("#####"), + packRow("...#."), + packRow("...#.")); +MINI_ICON(ICON_DIGIT_5, 5, + packRow("#####"), + packRow("#...."), + packRow("####."), + packRow("....#"), + packRow("....#"), + packRow("#...#"), + packRow(".###.")); +MINI_ICON(ICON_DIGIT_6, 5, + packRow("..##."), + packRow(".#..."), + packRow("#...."), + packRow("####."), + packRow("#...#"), + packRow("#...#"), + packRow(".###.")); +MINI_ICON(ICON_DIGIT_7, 5, + packRow("#####"), + packRow("....#"), + packRow("...#."), + packRow("..#.."), + packRow(".#..."), + packRow(".#..."), + packRow(".#...")); +MINI_ICON(ICON_DIGIT_8, 5, + packRow(".###."), + packRow("#...#"), + packRow("#...#"), + packRow(".###."), + packRow("#...#"), + packRow("#...#"), + packRow(".###.")); +MINI_ICON(ICON_DIGIT_9, 5, + packRow(".###."), + packRow("#...#"), + packRow("#...#"), + packRow(".####"), + packRow("....#"), + packRow("...#."), + packRow(".##..")); +#else MINI_ICON(ICON_DIGIT_0, 3, packRow("###"), packRow("#.#"), @@ -203,6 +416,8 @@ MINI_ICON(ICON_DIGIT_9, 3, packRow("..#"), packRow("###")); +#endif + 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, @@ -215,7 +430,7 @@ static constexpr const MiniIcon* MINI_ICON_DIGITS[10] = { 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; + return digits * ICON_DIGIT_0.w * s + (digits - 1) * s; } // Draws `n` (clamped to 0-99) as a left-to-right run of tiny digit icons — @@ -223,14 +438,108 @@ inline int miniIconNumberWidth(DisplayDriver& d, int n) { // 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); + // The 9x15's digits stand on its baseline a pixel below the centred box. + if (MINI_ICONS_LARGE == 2) top_y += 1; 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; } + if (n >= 10) { miniIconDraw(d, x, top_y, *MINI_ICON_DIGITS[n / 10]); x += ICON_DIGIT_0.w * s + s; } miniIconDraw(d, x, top_y, *MINI_ICON_DIGITS[n % 10]); } // Top-bar status glyphs (replace the single-letter M / B / A indicators). -#if MINI_ICONS_LARGE +#if MINI_ICONS_LARGE == 2 +MINI_ICON(ICON_MUTE, 10, // speaker + cross (sound off) + packRow("...#......"), + packRow("..##......"), + packRow("####.#...#"), + packRow("####..#.#."), + packRow("####...#.."), + packRow("####..#.#."), + packRow("####.#...#"), + packRow("..##......"), + packRow("...#......")); +MINI_ICON(ICON_BLUETOOTH, 7, // ᛒ bluetooth rune + packRow("...#..."), + packRow("...##.."), + packRow("#..#.#."), + packRow(".#.##.."), + packRow("..##..."), + packRow(".#.##.."), + packRow("#..#.#."), + packRow("...##.."), + packRow("...#...")); +MINI_ICON(ICON_ADVERT, 9, // ((•)) advert + packRow(".#.....#."), + packRow("#.......#"), + packRow("#..###..#"), + packRow("#.#####.#"), + packRow("#.#####.#"), + packRow("#.#####.#"), + packRow("#..###..#"), + packRow("#.......#"), + packRow(".#.....#.")); +MINI_ICON(ICON_ALARM, 9, // bell — an alarm is armed + packRow("....#...."), + packRow("...###..."), + packRow("..#####.."), + packRow("..#####.."), + packRow("..#####.."), + packRow(".#######."), + packRow("#########"), + packRow("........."), + packRow("...###...")); +MINI_ICON(ICON_TRAIL, 7, // map pin (GPS trail logging) + packRow("..###.."), + packRow(".#####."), + packRow("##...##"), + packRow("##...##"), + packRow("##...##"), + packRow(".#####."), + packRow("..###.."), + packRow("...#..."), + packRow("...#...")); +MINI_ICON(ICON_REPEATER, 9, // » relaying (repeater active) + packRow("#...#...."), + packRow(".#...#..."), + packRow("..#...#.."), + packRow("...#...#."), + packRow("....#...#"), + packRow("...#...#."), + packRow("..#...#.."), + packRow(".#...#..."), + packRow("#...#....")); +MINI_ICON(ICON_GPS, 9, // reticle with its dot: GPS has a fix + packRow("...###..."), + packRow(".##...##."), + packRow(".#.....#."), + packRow("#...#...#"), + packRow("#..###..#"), + packRow("#...#...#"), + packRow(".#.....#."), + packRow(".##...##."), + packRow("...###...")); +MINI_ICON(ICON_GPS_SEARCH, 9, // the reticle broken: still searching + packRow("...#.#..."), + packRow(".##...##."), + packRow(".#.....#."), + packRow("#.......#"), + packRow("........."), + packRow("#.......#"), + packRow(".#.....#."), + packRow(".##...##."), + packRow("...#.#...")); + +MINI_ICON(ICON_CHART, 9, // ascending bars — Home › Status + packRow("........#"), + packRow("......#.#"), + packRow("......#.#"), + packRow("....#.#.#"), + packRow("....#.#.#"), + packRow("..#.#.#.#"), + packRow("..#.#.#.#"), + packRow("#.#.#.#.#"), + packRow("#########")); +#elif MINI_ICONS_LARGE MINI_ICON(ICON_MUTE, 8, // speaker + cross (sound off) packRow("...#...."), packRow("..##...."), @@ -377,7 +686,106 @@ MINI_ICON(ICON_CHART, 5, // ascending bars — Home › Status // 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(). -#if MINI_ICONS_LARGE +#if MINI_ICONS_LARGE == 2 +MINI_ICON(ICON_PG_CLOCK, 9, // clock face + hands + packRow("...###..."), + packRow(".##.#.##."), + packRow(".#..#..#."), + packRow("#...#...#"), + packRow("#...###.#"), + packRow("#.......#"), + packRow(".#.....#."), + packRow(".##...##."), + packRow("...###...")); +MINI_ICON(ICON_PG_STAR, 9, // favourites + packRow("....#...."), + packRow("....#...."), + packRow("...###..."), + packRow("#########"), + packRow(".#######."), + packRow("..#####.."), + packRow("..##.##.."), + packRow(".##...##."), + packRow(".#.....#.")); +MINI_ICON(ICON_PG_RADIO, 9, // antenna with waves — radio + packRow(".#.....#."), + packRow("#...#...#"), + packRow("#..###..#"), + packRow("#...#...#"), + packRow(".#..#..#."), + packRow("....#...."), + packRow("...#.#..."), + packRow("..#...#.."), + packRow(".#.....#.")); +MINI_ICON(ICON_PG_BT, 7, // bluetooth + packRow("...#..."), + packRow("...##.."), + packRow("#..#.#."), + packRow(".#.##.."), + packRow("..##..."), + packRow(".#.##.."), + packRow("#..#.#."), + packRow("...##.."), + packRow("...#...")); +MINI_ICON(ICON_PG_ADVERT, 9, // advert page: the same waves + packRow(".#.....#."), + packRow("#.......#"), + packRow("#..###..#"), + packRow("#.#####.#"), + packRow("#.#####.#"), + packRow("#.#####.#"), + packRow("#..###..#"), + packRow("#.......#"), + packRow(".#.....#.")); +MINI_ICON(ICON_PG_SETTINGS, 9, // cog with a hub hole + packRow("...#.#..."), + packRow(".#######."), + packRow(".##...##."), + packRow("##.....##"), + packRow(".#.....#."), + packRow("##.....##"), + packRow(".##...##."), + packRow(".#######."), + packRow("...#.#...")); +MINI_ICON(ICON_PG_MAP, 10, // folded map + packRow("##########"), + packRow("#..#..#..#"), + packRow("#..#..#..#"), + packRow("#..#..#..#"), + packRow("#..#..#..#"), + packRow("#..#..#..#"), + packRow("#..#..#..#"), + packRow("#..#..#..#"), + packRow("##########")); +MINI_ICON(ICON_PG_TOOLS, 9, // wrench, open jaw top right + packRow("......#.#"), + packRow("......#.#"), + packRow("......###"), + packRow(".....##.."), + packRow("....##..."), + packRow("...##...."), + packRow("..##....."), + packRow(".##......"), + packRow("##.......")); +MINI_ICON(ICON_PG_MSG, 9, // speech bubble with three dots + packRow("#########"), + packRow("#.......#"), + packRow("#.#.#.#.#"), + packRow("#.......#"), + packRow("#########"), + packRow("##......."), + packRow("#........")); +MINI_ICON(ICON_PG_POWER, 9, // power symbol + packRow("....#...."), + packRow("..#.#.#.."), + packRow(".#..#..#."), + packRow("#...#...#"), + packRow("#.......#"), + packRow("#.......#"), + packRow(".#.....#."), + packRow("..#...#.."), + packRow("...###...")); +#elif MINI_ICONS_LARGE MINI_ICON(ICON_PG_CLOCK, 7, // clock face + hands packRow("..###.."), packRow(".#.#.#."), @@ -522,15 +930,44 @@ MINI_ICON(ICON_PG_POWER, 5, // power symbol — shutdown // Trail-map markers — centred on a point (see miniIconDrawCentered) rather // than anchored to a text line. +#if MINI_ICONS_LARGE == 2 +MINI_ICON(ICON_MAP_DOT, 5, // ● filled trail point + packRow("..#.."), + packRow(".###."), + packRow("#####"), + packRow(".###."), + packRow("..#..")); +#else MINI_ICON(ICON_MAP_DOT, 3, // ● filled trail point packRow("###"), packRow("###"), packRow("###")); +#endif +#if MINI_ICONS_LARGE == 2 +MINI_ICON(ICON_MAP_RING, 5, // ○ hollow ring — a new trail segment start + packRow(".###."), + packRow("#...#"), + packRow("#...#"), + packRow("#...#"), + packRow(".###.")); +#else MINI_ICON(ICON_MAP_RING, 3, // ○ hollow ring — marks a new trail segment start packRow("###"), packRow("#.#"), packRow("###")); -#if MINI_ICONS_LARGE // the markers that also head rows of text +#endif +#if MINI_ICONS_LARGE == 2 // the markers that also head rows of text +MINI_ICON(ICON_MAP_WAYPOINT, 9, // ◇ hollow diamond — saved waypoint + packRow("....#...."), + packRow("...#.#..."), + packRow("..#...#.."), + packRow(".#.....#."), + packRow("#.......#"), + packRow(".#.....#."), + packRow("..#...#.."), + packRow("...#.#..."), + packRow("....#....")); +#elif MINI_ICONS_LARGE MINI_ICON(ICON_MAP_WAYPOINT, 7, // ◇ hollow diamond — saved waypoint packRow("...#..."), packRow("..#.#.."), @@ -547,19 +984,56 @@ MINI_ICON(ICON_MAP_WAYPOINT, 5, // ◇ hollow diamond — saved waypoint packRow(".#.#."), packRow("..#..")); #endif +#if MINI_ICONS_LARGE == 2 +MINI_ICON(ICON_MAP_START, 9, // + trail start marker + packRow("....#...."), + packRow("....#...."), + packRow("....#...."), + packRow("....#...."), + packRow("#########"), + packRow("....#...."), + packRow("....#...."), + packRow("....#...."), + packRow("....#....")); +#else MINI_ICON(ICON_MAP_START, 5, // + trail start marker packRow("..#.."), packRow("..#.."), packRow("#####"), packRow("..#.."), packRow("..#..")); +#endif +#if MINI_ICONS_LARGE == 2 +MINI_ICON(ICON_MAP_CURRENT, 9, // ✕ live position / last trail point + packRow("##.....##"), + packRow("###...###"), + packRow(".###.###."), + packRow("..#####.."), + packRow("...###..."), + packRow("..#####.."), + packRow(".###.###."), + packRow("###...###"), + packRow("##.....##")); +#else MINI_ICON(ICON_MAP_CURRENT, 5, // ✕ live position / last trail point packRow("#...#"), packRow(".#.#."), packRow("..#.."), packRow(".#.#."), packRow("#...#")); -#if MINI_ICONS_LARGE +#endif +#if MINI_ICONS_LARGE == 2 +MINI_ICON(ICON_MAP_CONTACT, 9, // ◆ filled diamond — a live-tracked contact + packRow("....#...."), + packRow("...###..."), + packRow("..#####.."), + packRow(".#######."), + packRow("#########"), + packRow(".#######."), + packRow("..#####.."), + packRow("...###..."), + packRow("....#....")); +#elif MINI_ICONS_LARGE MINI_ICON(ICON_MAP_CONTACT, 7, // ◆ filled diamond — a live-tracked contact packRow("...#..."), packRow("..###.."), @@ -576,6 +1050,19 @@ MINI_ICON(ICON_MAP_CONTACT, 5, // ◆ filled diamond — a live-tracked conta packRow(".###."), packRow("..#..")); #endif +#if MINI_ICONS_LARGE == 2 +MINI_ICON(ICON_MAP_NORTH, 7, // "N" with a peaked roof — compass north marker + packRow("...#..."), + packRow("..###.."), + packRow(".#...#."), + packRow("#.....#"), + packRow("##....#"), + packRow("#.#...#"), + packRow("#..#..#"), + packRow("#...#.#"), + packRow("#....##"), + packRow("#.....#")); +#else MINI_ICON(ICON_MAP_NORTH, 5, // "N" with a peaked roof — compass north marker packRow("..#.."), packRow(".###."), @@ -584,15 +1071,111 @@ MINI_ICON(ICON_MAP_NORTH, 5, // "N" with a peaked roof — compass north ma packRow("#.#.#"), packRow("#..##"), packRow("#...#")); +#endif +#if MINI_ICONS_LARGE == 2 +MINI_ICON(ICON_MAP_TARGET, 7, // ⚑ flag on a pole — the active Locator/Nav target + packRow("#######"), + packRow("#.....#"), + packRow("#.....#"), + packRow("#######"), + packRow("#......"), + packRow("#......"), + packRow("#......"), + packRow("#......"), + packRow("#......")); +#else MINI_ICON(ICON_MAP_TARGET, 5, // ⚑ flag on a pole — the active Locator/Nav target packRow("####."), packRow("#..#."), packRow("####."), packRow("#...."), packRow("#....")); +#endif // Arrows towards the eight compass points (north up), for a bearing beside // a distance: ICON_ARROWS[((deg + 22) % 360) / 45]. +#if MINI_ICONS_LARGE == 2 +MINI_ICON(ICON_ARROW_N, 9, + packRow("....#...."), + packRow("...###..."), + packRow("..#.#.#.."), + packRow(".#..#..#."), + packRow("....#...."), + packRow("....#...."), + packRow("....#...."), + packRow("....#...."), + packRow("....#....")); +MINI_ICON(ICON_ARROW_NE, 9, + packRow("....#####"), + packRow(".......##"), + packRow("......#.#"), + packRow(".....#..#"), + packRow("....#...#"), + packRow("...#....."), + packRow("..#......"), + packRow(".#......."), + packRow("#........")); +MINI_ICON(ICON_ARROW_E, 9, + packRow("........."), + packRow(".....#..."), + packRow("......#.."), + packRow(".......#."), + packRow("#########"), + packRow(".......#."), + packRow("......#.."), + packRow(".....#..."), + packRow(".........")); +MINI_ICON(ICON_ARROW_SE, 9, + packRow("#........"), + packRow(".#......."), + packRow("..#......"), + packRow("...#....."), + packRow("....#...#"), + packRow(".....#..#"), + packRow("......#.#"), + packRow(".......##"), + packRow("....#####")); +MINI_ICON(ICON_ARROW_S, 9, + packRow("....#...."), + packRow("....#...."), + packRow("....#...."), + packRow("....#...."), + packRow("....#...."), + packRow(".#..#..#."), + packRow("..#.#.#.."), + packRow("...###..."), + packRow("....#....")); +MINI_ICON(ICON_ARROW_SW, 9, + packRow("........#"), + packRow(".......#."), + packRow("......#.."), + packRow(".....#..."), + packRow("#...#...."), + packRow("#..#....."), + packRow("#.#......"), + packRow("##......."), + packRow("#####....")); +MINI_ICON(ICON_ARROW_W, 9, + packRow("........."), + packRow("...#....."), + packRow("..#......"), + packRow(".#......."), + packRow("#########"), + packRow(".#......."), + packRow("..#......"), + packRow("...#....."), + packRow(".........")); +MINI_ICON(ICON_ARROW_NW, 9, + packRow("#####...."), + packRow("##......."), + packRow("#.#......"), + packRow("#..#....."), + packRow("#...#...."), + packRow(".....#..."), + packRow("......#.."), + packRow(".......#."), + packRow("........#")); +#else MINI_ICON(ICON_ARROW_N, 7, packRow("...#..."), packRow("..###.."), @@ -657,10 +1240,51 @@ MINI_ICON(ICON_ARROW_NW, 7, packRow("....#.."), packRow(".....#."), packRow("......#")); +#endif static constexpr const MiniIcon* ICON_ARROWS[8] = { &ICON_ARROW_N, &ICON_ARROW_NE, &ICON_ARROW_E, &ICON_ARROW_SE, &ICON_ARROW_S, &ICON_ARROW_SW, &ICON_ARROW_W, &ICON_ARROW_NW }; // Keyboard special-key glyphs. +#if MINI_ICONS_LARGE == 2 +MINI_ICON(ICON_KEYBOARD, 9, // PIN keyboard to ABC keyboard switch icon + packRow("#.#.#.#.#"), + packRow("#########"), + packRow("#.#.#.#.#"), + packRow("#########"), + packRow("#.#.#.#.#")); +MINI_ICON(ICON_SHIFT, 9, // ⇧ caps + packRow("....#...."), + packRow("...###..."), + packRow("..#####.."), + packRow(".#######."), + packRow("#########"), + packRow("...###..."), + packRow("...###..."), + packRow("...###..."), + packRow("...###...")); +MINI_ICON(ICON_BACKSPACE, 11, // ⌫ delete-left (× knocked out of the arrow body) + packRow("....#######"), + packRow("...########"), + packRow("..###.###.#"), + packRow(".#####.#.##"), + packRow("#######.###"), + packRow(".#####.#.##"), + packRow("..###.###.#"), + packRow("...########"), + packRow("....#######")); +MINI_ICON(ICON_SPACE_L, 7, + packRow("#......"), + packRow("#......"), + packRow("#......"), + packRow("#......"), + packRow("#######")); +MINI_ICON(ICON_SPACE_R, 7, + packRow("......#"), + packRow("......#"), + packRow("......#"), + packRow("......#"), + packRow("#######")); +#else MINI_ICON(ICON_KEYBOARD, 7, // PIN keyboard to ABC keyboard switch icon packRow("#.#.#.#"), packRow("#######"), @@ -682,7 +1306,7 @@ MINI_ICON(ICON_BACKSPACE, 8, // ⌫ delete-left (× knocked out of the arrow packRow(".###.#.#"), packRow("..######"), packRow("...#####")); -// Space ⎵ is wider than the 8-px mini-icon limit, so it is two halves drawn +// Space ⎵ 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("#......."), @@ -695,6 +1319,7 @@ MINI_ICON(ICON_SPACE_R, 8, packRow(".......#"), packRow("########")); +#endif // 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 @@ -784,8 +1409,10 @@ inline void drawScrollIndicator(DisplayDriver& d, int right_x, int top_y, int tr // `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). +// right_x narrows the list to a left column (a list beside a detail pane). template -inline int drawList(DisplayDriver& d, int total, int sel, int& scroll, RenderRow row) { +inline int drawList(DisplayDriver& d, int total, int sel, int& scroll, RenderRow row, int right_x = -1) { + if (right_x < 0) right_x = d.width(); const int item_h = d.lineStep(); const int start_y = d.listStart(); int visible = d.listVisible(item_h); @@ -796,7 +1423,7 @@ inline int drawList(DisplayDriver& d, int total, int sel, int& scroll, RenderRow 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); + drawScrollIndicator(d, right_x, start_y, visible * item_h, total, visible, scroll); return visible; } @@ -852,8 +1479,8 @@ inline int drawLoadingDots(DisplayDriver& d, int cx, int y_bottom) { // 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); +inline void drawRowSelection(DisplayDriver& d, int y, bool sel, int reserve, int right_x = -1) { + d.drawSelectionRow(0, y - 1, (right_x < 0 ? d.width() : right_x) - reserve, d.lineStep() - 1, sel); } // Favourite marker for a list row, on every screen that lists something