mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 02:11:57 +00:00
feat(l1): L2 splash, hovering Home icons, soft selections, two-line DM bubbles
- Splash: the L2 wordmark and lettering, shared through ui-core/Lettering.h, rise in with loading dots under the version line (static frame on e-ink). - Home: the header shows the time once the clock is set; icon pages carry only their title. One 32 px icon family (3 px strokes) bobs 2 px over a dithered blob shadow that shrinks as it rises; the unread badge rides on the Messages bubble; Bluetooth off is the icon at half density. - Soft corners on every selection, the page pill, tabs and popups; dotted scrollbar track with a rounded thumb; the tab bar lays out whole tabs and shows edge arrows only in free space. - DM bubbles drop the sender row and wrap to a second line, with the delivery mark and age closing it. - Keyboards name what is typed (channel, contact, Password, Scope...) and show characters left near the limit; PIN entry has its own preview. - Loading dots for waits (login, scan, GPS) and signal bars in place of raw RSSI in Nearby. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
#pragma once
|
||||
// The MeshCore wordmark and its lettering, shared by both boot splashes
|
||||
// (ui-new/UITask.cpp on the OLED/e-ink boards, ui-lvgl/Splash.h on the L2):
|
||||
// the 128x13 wordmark bitmap, its own letters, the extra characters drawn in
|
||||
// the same strokes (digits, '.', '-', v, l, d, space), and the Solo version /
|
||||
// build date strings both splashes show. Platform-free: callers plot pixels.
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
|
||||
namespace lettering {
|
||||
static const int LOGO_W = 128, LOGO_H = 13;
|
||||
|
||||
// 'meshcore', 128x13 px, MSB first
|
||||
static const uint8_t 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,
|
||||
};
|
||||
|
||||
// The wordmark's own letters: columns of LOGO.
|
||||
struct LogoLetter { char c; uint8_t x0, w; };
|
||||
static const LogoLetter LOGO_LETTERS[] = {
|
||||
{ 'm', 0, 18 }, { 'e', 19, 15 }, { 's', 35, 15 }, { 'h', 50, 16 }, { 'c', 66, 14 }, { 'o', 80, 16 }, { 'r', 96, 16 },
|
||||
};
|
||||
// The rest, drawn upright in the same strokes (3 px bars at the top, middle
|
||||
// and bottom); slanted like the wordmark when drawn (SLANT).
|
||||
struct PixGlyph { char c; const char* rows[LOGO_H]; };
|
||||
#define F10 "##########"
|
||||
#define L10 "###......."
|
||||
#define R10 ".......###"
|
||||
#define B10 "###....###"
|
||||
static const PixGlyph GLYPHS[] = {
|
||||
{ '0', { F10, F10, F10, B10, B10, B10, B10, B10, B10, B10, F10, F10, F10 } },
|
||||
{ '1', { "....####..", "...#####..", "..######..", "....####..", "....####..", "....####..", "....####..",
|
||||
"....####..", "....####..", "....####..", "....####..", "....####..", "....####.." } },
|
||||
{ '2', { F10, F10, F10, R10, R10, F10, F10, F10, L10, L10, F10, F10, F10 } },
|
||||
{ '3', { F10, F10, F10, R10, R10, F10, F10, F10, R10, R10, F10, F10, F10 } },
|
||||
{ '4', { B10, B10, B10, B10, B10, F10, F10, F10, R10, R10, R10, R10, R10 } },
|
||||
{ '5', { F10, F10, F10, L10, L10, F10, F10, F10, R10, R10, F10, F10, F10 } },
|
||||
{ '6', { F10, F10, F10, L10, L10, F10, F10, F10, B10, B10, F10, F10, F10 } },
|
||||
{ '7', { F10, F10, F10, R10, R10, R10, R10, R10, R10, R10, R10, R10, R10 } },
|
||||
{ '8', { F10, F10, F10, B10, B10, F10, F10, F10, B10, B10, F10, F10, F10 } },
|
||||
{ '9', { F10, F10, F10, B10, B10, F10, F10, F10, R10, R10, F10, F10, F10 } },
|
||||
{ '.', { "....", "....", "....", "....", "....", "....", "....", "....", "....", "....", "###.", "###.", "###." } },
|
||||
{ '-', { ".......", ".......", ".......", ".......", ".......", "#######", "#######", "#######",
|
||||
".......", ".......", ".......", ".......", "......." } },
|
||||
{ 'v', { "###.....###", "###.....###", "###.....###", "###.....###", "###.....###", "###.....###", "###.....###",
|
||||
"###.....###", ".###...###.", ".###...###.", "..###.###..", "...#####...", "....###...." } },
|
||||
{ 'l', { "###......", "###......", "###......", "###......", "###......", "###......", "###......",
|
||||
"###......", "###......", "###......", "#########", "#########", "#########" } },
|
||||
{ 'd', { "#########.", F10, F10, B10, B10, B10, B10, B10, B10, B10, F10, F10, "#########." } },
|
||||
{ ' ', { ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", "....." } },
|
||||
};
|
||||
#undef F10
|
||||
#undef L10
|
||||
#undef R10
|
||||
#undef B10
|
||||
static const uint8_t SLANT[LOGO_H] = { 2, 2, 2, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 }; // px right, per row
|
||||
|
||||
static inline bool logoBit(int x, int y) {
|
||||
return LOGO[y * (LOGO_W / 8) + x / 8] & (0x80 >> (x % 8));
|
||||
}
|
||||
|
||||
// One character of the lettering: its width, and whether (x, y) is inked.
|
||||
static inline const LogoLetter* logoLetter(char c) {
|
||||
for (const LogoLetter& l : LOGO_LETTERS) if (l.c == c) return &l;
|
||||
return nullptr;
|
||||
}
|
||||
static inline const PixGlyph* pixGlyph(char c) {
|
||||
for (const PixGlyph& g : GLYPHS) if (g.c == c) return &g;
|
||||
return nullptr;
|
||||
}
|
||||
static inline int charW(char c) {
|
||||
if (const LogoLetter* l = logoLetter(c)) return l->w;
|
||||
if (const PixGlyph* g = pixGlyph(c)) return (int)strlen(g->rows[0]) + 2; // + the slant
|
||||
return -1;
|
||||
}
|
||||
static inline bool inked(char c, int x, int y) {
|
||||
if (const LogoLetter* l = logoLetter(c)) {
|
||||
return logoBit(l->x0 + x, y);
|
||||
}
|
||||
const PixGlyph* g = pixGlyph(c);
|
||||
int gx = x - SLANT[y];
|
||||
return g && gx >= 0 && gx < (int)strlen(g->rows[y]) && g->rows[y][gx] == '#';
|
||||
}
|
||||
|
||||
// `text` in the lettering (lower case), -1 if it has a character that isn't drawn.
|
||||
static inline int textW(const char* text, int gap) {
|
||||
int w = 0;
|
||||
for (const char* p = text; *p; p++) {
|
||||
int cw = charW((char)tolower((unsigned char)*p));
|
||||
if (cw < 0) return -1;
|
||||
w += cw + (p[1] ? gap : 0);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
// "v1.28-solo.1-abcdef" -> "v1.28-solo.1": build.sh appends the commit as
|
||||
// the last dash segment .
|
||||
static inline void soloVersion(char* out, size_t n) {
|
||||
const char* ver = FIRMWARE_VERSION;
|
||||
const char* dash = strrchr(ver, '-');
|
||||
size_t len = dash ? (size_t)(dash - ver) : strlen(ver);
|
||||
if (len >= n) len = n - 1;
|
||||
memcpy(out, ver, len);
|
||||
out[len] = '\0';
|
||||
}
|
||||
|
||||
// The build date as digits, "2026-09-26": build.sh gives "26-Sep-2026",
|
||||
// __DATE__ "Sep 26 2026". Anything else is passed through.
|
||||
static inline void buildDate(char* out, size_t n) {
|
||||
static const char MON[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
|
||||
const char* d = FIRMWARE_BUILD_DATE;
|
||||
int day = 0, year = 0, mon = 0;
|
||||
char m[4] = "";
|
||||
if (sscanf(d, "%d-%3s-%d", &day, m, &year) != 3 && sscanf(d, "%3s %d %d", m, &day, &year) != 3) {
|
||||
snprintf(out, n, "%s", d);
|
||||
return;
|
||||
}
|
||||
const char* f = strstr(MON, m);
|
||||
mon = f && m[0] ? (int)(f - MON) / 3 + 1 : 0;
|
||||
if (mon) snprintf(out, n, "%04d-%02d-%02d", year, mon, day);
|
||||
else snprintf(out, n, "%s", d);
|
||||
}
|
||||
} // namespace lettering
|
||||
@@ -8,66 +8,12 @@
|
||||
// (status bar too) from lv_layer_top, fades out after SPLASH_MS; a tap skips
|
||||
// it. Home -- and the PIN lock, if set -- are already built underneath.
|
||||
|
||||
#include "../ui-core/Lettering.h"
|
||||
|
||||
namespace splash {
|
||||
static const uint32_t SPLASH_MS = 2500;
|
||||
static const int LOGO_W = 128, LOGO_H = 13, LOGO_SCALE = 2;
|
||||
|
||||
// 'meshcore', 128x13 px, MSB first (ui-new/icons.h meshcore_logo)
|
||||
static const uint8_t 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,
|
||||
};
|
||||
|
||||
// The wordmark's own letters: columns of LOGO.
|
||||
struct LogoLetter { char c; uint8_t x0, w; };
|
||||
static const LogoLetter LOGO_LETTERS[] = {
|
||||
{ 'm', 0, 18 }, { 'e', 19, 15 }, { 's', 35, 15 }, { 'h', 50, 16 }, { 'c', 66, 14 }, { 'o', 80, 16 }, { 'r', 96, 16 },
|
||||
};
|
||||
// The rest, drawn upright in the same strokes (3 px bars at the top, middle
|
||||
// and bottom); slanted like the wordmark when drawn (SLANT).
|
||||
struct PixGlyph { char c; const char* rows[LOGO_H]; };
|
||||
#define F10 "##########"
|
||||
#define L10 "###......."
|
||||
#define R10 ".......###"
|
||||
#define B10 "###....###"
|
||||
static const PixGlyph GLYPHS[] = {
|
||||
{ '0', { F10, F10, F10, B10, B10, B10, B10, B10, B10, B10, F10, F10, F10 } },
|
||||
{ '1', { "....####..", "...#####..", "..######..", "....####..", "....####..", "....####..", "....####..",
|
||||
"....####..", "....####..", "....####..", "....####..", "....####..", "....####.." } },
|
||||
{ '2', { F10, F10, F10, R10, R10, F10, F10, F10, L10, L10, F10, F10, F10 } },
|
||||
{ '3', { F10, F10, F10, R10, R10, F10, F10, F10, R10, R10, F10, F10, F10 } },
|
||||
{ '4', { B10, B10, B10, B10, B10, F10, F10, F10, R10, R10, R10, R10, R10 } },
|
||||
{ '5', { F10, F10, F10, L10, L10, F10, F10, F10, R10, R10, F10, F10, F10 } },
|
||||
{ '6', { F10, F10, F10, L10, L10, F10, F10, F10, B10, B10, F10, F10, F10 } },
|
||||
{ '7', { F10, F10, F10, R10, R10, R10, R10, R10, R10, R10, R10, R10, R10 } },
|
||||
{ '8', { F10, F10, F10, B10, B10, F10, F10, F10, B10, B10, F10, F10, F10 } },
|
||||
{ '9', { F10, F10, F10, B10, B10, F10, F10, F10, R10, R10, F10, F10, F10 } },
|
||||
{ '.', { "....", "....", "....", "....", "....", "....", "....", "....", "....", "....", "###.", "###.", "###." } },
|
||||
{ '-', { ".......", ".......", ".......", ".......", ".......", "#######", "#######", "#######",
|
||||
".......", ".......", ".......", ".......", "......." } },
|
||||
{ 'v', { "###.....###", "###.....###", "###.....###", "###.....###", "###.....###", "###.....###", "###.....###",
|
||||
"###.....###", ".###...###.", ".###...###.", "..###.###..", "...#####...", "....###...." } },
|
||||
{ 'l', { "###......", "###......", "###......", "###......", "###......", "###......", "###......",
|
||||
"###......", "###......", "###......", "#########", "#########", "#########" } },
|
||||
{ 'd', { "#########.", F10, F10, B10, B10, B10, B10, B10, B10, B10, F10, F10, "#########." } },
|
||||
{ ' ', { ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", ".....", "....." } },
|
||||
};
|
||||
#undef F10
|
||||
#undef L10
|
||||
#undef R10
|
||||
#undef B10
|
||||
static const uint8_t SLANT[LOGO_H] = { 2, 2, 2, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 }; // px right, per row
|
||||
using namespace lettering; // ../ui-core/Lettering.h: wordmark, letters, version/date
|
||||
static const int LOGO_SCALE = 2;
|
||||
|
||||
static lv_obj_t* s_root = nullptr;
|
||||
static const int MAX_IMGS = 6;
|
||||
@@ -95,41 +41,6 @@ static void onTimer(lv_timer_t* t) { (void)t; dismiss(); }
|
||||
static bool up() { return s_root != nullptr; } // until its fade-out has finished
|
||||
static void onTap(lv_event_t* e) { (void)e; dismiss(); }
|
||||
|
||||
// One character of the lettering: its width, and whether (x, y) is inked.
|
||||
static const LogoLetter* logoLetter(char c) {
|
||||
for (const LogoLetter& l : LOGO_LETTERS) if (l.c == c) return &l;
|
||||
return nullptr;
|
||||
}
|
||||
static const PixGlyph* pixGlyph(char c) {
|
||||
for (const PixGlyph& g : GLYPHS) if (g.c == c) return &g;
|
||||
return nullptr;
|
||||
}
|
||||
static int charW(char c) {
|
||||
if (const LogoLetter* l = logoLetter(c)) return l->w;
|
||||
if (const PixGlyph* g = pixGlyph(c)) return (int)strlen(g->rows[0]) + 2; // + the slant
|
||||
return -1;
|
||||
}
|
||||
static bool inked(char c, int x, int y) {
|
||||
if (const LogoLetter* l = logoLetter(c)) {
|
||||
int sx = l->x0 + x;
|
||||
return LOGO[y * (LOGO_W / 8) + sx / 8] & (0x80 >> (sx % 8));
|
||||
}
|
||||
const PixGlyph* g = pixGlyph(c);
|
||||
int gx = x - SLANT[y];
|
||||
return g && gx >= 0 && gx < (int)strlen(g->rows[y]) && g->rows[y][gx] == '#';
|
||||
}
|
||||
|
||||
// `text` in the lettering (lower case), -1 if it has a character that isn't drawn.
|
||||
static int textW(const char* text, int gap) {
|
||||
int w = 0;
|
||||
for (const char* p = text; *p; p++) {
|
||||
int cw = charW((char)tolower((unsigned char)*p));
|
||||
if (cw < 0) return -1;
|
||||
w += cw + (p[1] ? gap : 0);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
// A line in the lettering, `scale` x, in `col`: an image, or a Noto label when
|
||||
// the text has a character the lettering lacks. `text` null = the wordmark.
|
||||
static lv_obj_t* letters(lv_obj_t* parent, const char* text, int scale, int gap, uint32_t col, const lv_font_t* fallback) {
|
||||
@@ -147,7 +58,7 @@ static lv_obj_t* letters(lv_obj_t* parent, const char* text, int scale, int gap,
|
||||
if (!text) {
|
||||
for (int y = 0; y < LOGO_H; y++)
|
||||
for (int x = 0; x < LOGO_W; x++)
|
||||
if (LOGO[y * (LOGO_W / 8) + x / 8] & (0x80 >> (x % 8))) plot(x, y);
|
||||
if (logoBit(x, y)) plot(x, y);
|
||||
} else {
|
||||
int ox = 0;
|
||||
for (const char* p = text; *p; p++) {
|
||||
@@ -191,33 +102,6 @@ static void wave(lv_obj_t* dot, int i) {
|
||||
lv_anim_start(&a);
|
||||
}
|
||||
|
||||
// "v1.28-solo.1-abcdef" -> "v1.28-solo.1": build.sh appends the commit as
|
||||
// the last dash segment (as ui-new's splash).
|
||||
static void soloVersion(char* out, size_t n) {
|
||||
const char* ver = FIRMWARE_VERSION;
|
||||
const char* dash = strrchr(ver, '-');
|
||||
size_t len = dash ? (size_t)(dash - ver) : strlen(ver);
|
||||
if (len >= n) len = n - 1;
|
||||
memcpy(out, ver, len);
|
||||
out[len] = '\0';
|
||||
}
|
||||
|
||||
// The build date as digits, "2026-09-26": build.sh gives "26-Sep-2026",
|
||||
// __DATE__ "Sep 26 2026". Anything else is passed through.
|
||||
static void buildDate(char* out, size_t n) {
|
||||
static const char MON[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
|
||||
const char* d = FIRMWARE_BUILD_DATE;
|
||||
int day = 0, year = 0, mon = 0;
|
||||
char m[4] = "";
|
||||
if (sscanf(d, "%d-%3s-%d", &day, m, &year) != 3 && sscanf(d, "%3s %d %d", m, &day, &year) != 3) {
|
||||
snprintf(out, n, "%s", d);
|
||||
return;
|
||||
}
|
||||
const char* f = strstr(MON, m);
|
||||
mon = f && m[0] ? (int)(f - MON) / 3 + 1 : 0;
|
||||
if (mon) snprintf(out, n, "%04d-%02d-%02d", year, mon, day);
|
||||
else snprintf(out, n, "%s", d);
|
||||
}
|
||||
} // namespace splash
|
||||
|
||||
void UITask::showSplash() {
|
||||
|
||||
@@ -61,6 +61,7 @@ class AdminScreen : public UIScreen {
|
||||
|
||||
void openLoginKb() {
|
||||
kb().begin("", 15); // admin password: same max length as room/repeater login
|
||||
kb().prompt = "Password";
|
||||
kb().clearPlaceholders(); // {loc}/{time} are for messages, not a password
|
||||
_login_kb = true;
|
||||
}
|
||||
@@ -69,6 +70,7 @@ class AdminScreen : public UIScreen {
|
||||
// {} picker to CLI command-name completion (Custom-command row only).
|
||||
void openValueKb(const char* initial, bool cli_autocomplete) {
|
||||
kb().begin(initial, 160);
|
||||
kb().prompt = "Command";
|
||||
kb().clearPlaceholders(); // a CLI command/value is literal, not a message
|
||||
if (cli_autocomplete) kb().setPlaceholderRefresh(refreshCmdPlaceholders, nullptr, "Commands:");
|
||||
_kb_active = true;
|
||||
@@ -162,6 +164,13 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// A waiting line centred under the header, with the loading dots below it.
|
||||
int waiting(DisplayDriver& display, const char* what) {
|
||||
int mid = (display.listStart() + display.height()) / 2;
|
||||
display.drawTextCentered(display.width() / 2, mid - display.lineStep(), what);
|
||||
return drawLoadingDots(display, display.width() / 2, mid + display.lineStep());
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
@@ -170,9 +179,8 @@ public:
|
||||
AdminSession::State st = S().state();
|
||||
if (st == AdminSession::LOGGING_IN || st == AdminSession::IDLE || st == AdminSession::NEED_PASSWORD) {
|
||||
display.drawCenteredHeader("ADMIN LOGIN");
|
||||
display.setCursor(2, display.listStart());
|
||||
if (st == AdminSession::LOGGING_IN) display.print("Logging in...");
|
||||
return 500;
|
||||
if (st != AdminSession::LOGGING_IN) return 500;
|
||||
return waiting(display, "Logging in");
|
||||
}
|
||||
if (_showing_reply) return _reply_view.render(display, S().target().name, S().reply(), false, false);
|
||||
if (_kb_active) return kb().render(display);
|
||||
@@ -180,9 +188,7 @@ public:
|
||||
char title[24];
|
||||
snprintf(title, sizeof(title), "%.23s", S().target().name);
|
||||
display.drawCenteredHeader(title);
|
||||
display.setCursor(2, display.listStart());
|
||||
display.print(S().fetching() ? "Fetching..." : "Waiting for reply...");
|
||||
return 500;
|
||||
return waiting(display, S().fetching() ? "Fetching" : "Waiting for reply");
|
||||
}
|
||||
|
||||
tabbar::draw(display, admin::TAB_LABELS, admin::TAB_COUNT, _tab);
|
||||
|
||||
@@ -205,6 +205,7 @@ public:
|
||||
if (char* txt = botcfg::text(_prefs, k, cap)) {
|
||||
_kb_row = _sel;
|
||||
_kb->begin(txt, cap - 1);
|
||||
_kb->prompt = botcfg::isTrigger(k) ? "Trigger" : "Reply";
|
||||
if (botcfg::isTrigger(k)) {
|
||||
_kb->clearPlaceholders(); // trigger is literal — placeholders never match
|
||||
} else {
|
||||
|
||||
@@ -51,8 +51,9 @@ class ChannelsView {
|
||||
|
||||
KeyboardWidget& kb() { return _task->keyboard(); }
|
||||
|
||||
void openKb(const char* initial, int max) {
|
||||
void openKb(const char* initial, int max, const char* prompt) {
|
||||
kb().begin(initial, max);
|
||||
kb().prompt = prompt;
|
||||
kb().clearPlaceholders(); // literal field — {loc}/{time} make no sense here
|
||||
_kb_active = true;
|
||||
}
|
||||
@@ -195,7 +196,7 @@ public:
|
||||
if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : 1; return true; }
|
||||
if (c == KEY_DOWN) { _sel = (_sel < 1) ? _sel + 1 : 0; return true; }
|
||||
if (c == KEY_ENTER) {
|
||||
if (_sel == 0) { _kb_field = 2; openKb(_topic, sizeof(_topic) - 1); }
|
||||
if (_sel == 0) { _kb_field = 2; openKb(_topic, sizeof(_topic) - 1, "Hashtag"); }
|
||||
else commit();
|
||||
}
|
||||
return true;
|
||||
@@ -210,8 +211,8 @@ public:
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_ENTER) {
|
||||
if (_sel == 0) { _kb_field = 0; openKb(_name, sizeof(_name) - 1); }
|
||||
else if (_sel == 1) { _kb_field = 1; openKb(_secret_text, _hex_mode ? 32 : (int)sizeof(_secret_text) - 1); }
|
||||
if (_sel == 0) { _kb_field = 0; openKb(_name, sizeof(_name) - 1, "Name"); }
|
||||
else if (_sel == 1) { _kb_field = 1; openKb(_secret_text, _hex_mode ? 32 : (int)sizeof(_secret_text) - 1, "Secret"); }
|
||||
else commit();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ class ClockToolsScreen : public UIScreen {
|
||||
int cx = valx + k * cw;
|
||||
if (k == char_idx) {
|
||||
d.setColor(DisplayDriver::DARK);
|
||||
d.fillRect(cx, y - 1, cw, d.getLineHeight() + 1);
|
||||
d.fillSoftRect(cx, y - 1, cw, d.getLineHeight() + 1);
|
||||
d.setColor(DisplayDriver::LIGHT);
|
||||
} else {
|
||||
d.setColor(DisplayDriver::DARK);
|
||||
|
||||
@@ -77,7 +77,7 @@ struct DigitEditor {
|
||||
int cx = x + k * cw;
|
||||
if (k == char_idx) {
|
||||
d.setColor(DisplayDriver::DARK);
|
||||
d.fillRect(cx, y - 1, cw, d.getLineHeight() + 1);
|
||||
d.fillSoftRect(cx, y - 1, cw, d.getLineHeight() + 1);
|
||||
d.setColor(DisplayDriver::LIGHT);
|
||||
} else {
|
||||
d.setColor(DisplayDriver::DARK);
|
||||
|
||||
@@ -271,7 +271,10 @@ struct KeyboardWidget {
|
||||
// The number pad (beginPin) instead of the user's ABC / T9 layout; its ABC
|
||||
// key drops back to that layout for a password with letters.
|
||||
bool pin_mode = false;
|
||||
const char* pin_prompt = nullptr; // e.g. "New PIN", shown in the preview; kept past the ABC switch
|
||||
// What is being typed ("New PIN", "Password", a channel name...), shown at
|
||||
// the right of the preview; set it after begin(), which clears it. Kept past
|
||||
// the PIN pad's ABC switch.
|
||||
const char* prompt = nullptr;
|
||||
static const int PIN_MAX_LEN = 16;
|
||||
int row, col;
|
||||
int page; // see totalPages()/scriptAt()/pageIsSymbols() below
|
||||
@@ -416,7 +419,7 @@ struct KeyboardWidget {
|
||||
caps_lock = false;
|
||||
pin_kb_mask_enabled = false;
|
||||
pin_mode = false;
|
||||
pin_prompt = nullptr;
|
||||
prompt = nullptr;
|
||||
t9_cell = -1;
|
||||
t9_cycle = 0;
|
||||
_ph_menu.active = false;
|
||||
@@ -430,10 +433,10 @@ struct KeyboardWidget {
|
||||
}
|
||||
|
||||
// Open keyboard in PIN mode
|
||||
void beginPin(const char* initial = "", int max = PIN_MAX_LEN, bool mask = false, const char* prompt = nullptr) {
|
||||
void beginPin(const char* initial = "", int max = PIN_MAX_LEN, bool mask = false, const char* label = nullptr) {
|
||||
begin(initial, max);
|
||||
pin_mode = true;
|
||||
pin_prompt = prompt;
|
||||
prompt = label;
|
||||
pin_kb_mask_enabled = mask; // Mask input of number field
|
||||
}
|
||||
|
||||
@@ -518,6 +521,29 @@ struct KeyboardWidget {
|
||||
else if (key == KEY_DOWN) { cursor_pos = len; }
|
||||
}
|
||||
|
||||
// PIN preview: what it's for, centred, and the digits below it spaced out
|
||||
// (filled soft squares when masked), with an underline on the next slot.
|
||||
void renderPinPreview(DisplayDriver& display, int sep_y) {
|
||||
const int lh = display.getLineHeight(), cw = display.getCharWidth();
|
||||
const int pitch = cw + 3;
|
||||
const int y = sep_y - lh; // digits on the last preview line
|
||||
int title_y = y - lh - 1;
|
||||
if (title_y < 0) title_y = 0;
|
||||
if (prompt && title_y + lh <= y) display.drawTextCentered(display.width() / 2, title_y, prompt);
|
||||
const int slots = len + (len < max_len ? 1 : 0);
|
||||
int x = display.width() / 2 - (slots * pitch - 3) / 2;
|
||||
for (int i = 0; i < slots; i++, x += pitch) {
|
||||
if (i == len) { display.fillRect(x, y + lh - 2, cw, 1); continue; } // next digit goes here
|
||||
if (pin_kb_mask_enabled) {
|
||||
display.fillSoftRect(x, y + 1, cw, lh - 3);
|
||||
} else {
|
||||
char d[2] = { buf[i], 0 };
|
||||
display.setCursor(x, y);
|
||||
display.print(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) {
|
||||
_visible = true;
|
||||
// A stale mid-cycle T9 press (no further input since) finalizes on its own —
|
||||
@@ -576,6 +602,9 @@ struct KeyboardWidget {
|
||||
// ...and the byte offset that line starts at.
|
||||
int ps = 0;
|
||||
for (int n = first_line * cpl; n > 0 && ps < len; n--) ps += kbUtf8CharBytesAt(buf, ps, len);
|
||||
if (isPin()) {
|
||||
renderPinPreview(display, sep_y);
|
||||
} else
|
||||
for (int pl = 0; pl < prev_lines; pl++) {
|
||||
int pe = ps; // byte offset cpl codepoints further along (or end of text)
|
||||
for (int k = 0; k < cpl && pe < len; k++) pe += kbUtf8CharBytesAt(buf, pe, len);
|
||||
@@ -622,9 +651,27 @@ struct KeyboardWidget {
|
||||
display.print(linebuf_t);
|
||||
ps = pe;
|
||||
}
|
||||
if (pin_prompt) { // what the PIN is for, bottom-right of the preview (alerts don't show over the keyboard)
|
||||
display.setCursor(display.width() - display.getTextWidth(pin_prompt), (prev_lines - 1) * lh);
|
||||
display.print(pin_prompt);
|
||||
if (!isPin()) {
|
||||
// Bottom-right of the preview: what the field is, or near the limit how
|
||||
// much room is left. Only when it clears the text on that line.
|
||||
char left[12];
|
||||
const char* tag = prompt;
|
||||
if (max_len > 0 && len * 4 >= max_len * 3) { snprintf(left, sizeof(left), "%d left", max_len - len); tag = left; }
|
||||
if (tag) {
|
||||
int tw = display.getTextWidth(tag);
|
||||
// Codepoints on the last preview line, plus the cursor if it's there.
|
||||
int total_chars = 0;
|
||||
for (int p = 0; p < len; ) { p += kbUtf8CharBytesAt(buf, p, len); total_chars++; }
|
||||
const int last_line = first_line + prev_lines - 1;
|
||||
int last_chars = total_chars - last_line * cpl;
|
||||
if (last_chars < 0) last_chars = 0;
|
||||
if (last_chars > cpl) last_chars = cpl;
|
||||
if (cursor_line == last_line) last_chars++;
|
||||
if (last_chars * cw + tw + cw <= display.width()) {
|
||||
display.setCursor(display.width() - tw, (prev_lines - 1) * lh);
|
||||
display.print(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
display.fillRect(0, sep_y, display.width(), display.sepH());
|
||||
|
||||
@@ -808,7 +855,7 @@ struct KeyboardWidget {
|
||||
int x = bx + i * seg_w;
|
||||
if (i == accent_sel) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(x + 1, by + 1, seg_w - 1, bh - 2);
|
||||
display.fillSoftRect(x + 1, by + 1, seg_w - 1, bh - 2);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
@@ -154,6 +154,46 @@ class MessagesScreen : public UIScreen {
|
||||
// reflow as messages arrive.
|
||||
struct HistScroll { bool need; int reserve; long total_px, scroll_px; int view_px; };
|
||||
|
||||
// Compact DM bubble text: -1 when it fits on one line beside the marker and
|
||||
// age (tail_w), else the byte offset its second line starts at -- the last
|
||||
// space that keeps line 1 within line_w, or a hard cut if there is none.
|
||||
static int dmSplit(DisplayDriver& d, const char* body, int line_w, int tail_w) {
|
||||
if ((int)d.getTextWidth(body) + tail_w <= line_w) return -1;
|
||||
const uint8_t* p = (const uint8_t*)body;
|
||||
int w = 0, last_sp = -1;
|
||||
while (*p) {
|
||||
const uint8_t* cp_start = p;
|
||||
uint32_t cp = DisplayDriver::decodeCodepoint(p);
|
||||
int off = (int)(cp_start - (const uint8_t*)body);
|
||||
if (cp == ' ' && w <= line_w) last_sp = off;
|
||||
w += d.getCodepointWidth(cp);
|
||||
if (w > line_w) return last_sp > 0 ? last_sp + 1 : (off > 0 ? off : (int)(p - (const uint8_t*)body));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Scrollbar metrics for boxes of varying height (h(idx, reserve) px each,
|
||||
// `gap` px apart): the same fit test as the portrait path below.
|
||||
template <class GetH>
|
||||
HistScroll computeHistScrollH(DisplayDriver& display, int count, int scroll, int hist_start_y, int cby,
|
||||
int gap, GetH h) {
|
||||
HistScroll r{};
|
||||
r.view_px = cby - (hist_start_y + 1);
|
||||
if (r.view_px < 1) r.view_px = 1;
|
||||
const int col = scrollIndicatorColWidth(display);
|
||||
int cur = hist_start_y, fit = 0;
|
||||
for (int i = 0; i < count; i++) { int bh = h(i, 0); if (cur + bh > cby) break; fit++; cur += bh + gap; }
|
||||
r.need = fit < count;
|
||||
r.reserve = r.need ? col : 0;
|
||||
if (r.need)
|
||||
for (int i = 0; i < count; i++) {
|
||||
long ext = h(i, r.reserve) + gap;
|
||||
r.total_px += ext;
|
||||
if (i < scroll) r.scroll_px += ext;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// `getBody(idx)` returns the body text for list item idx (the part that wraps),
|
||||
// or nullptr to fall back to a fixed 2-line box. Must return exactly what the
|
||||
// real per-item render pass wraps (sender split off, reply prefix stripped) --
|
||||
@@ -169,7 +209,7 @@ class MessagesScreen : public UIScreen {
|
||||
if (r.view_px < 1) r.view_px = 1;
|
||||
const int col = scrollIndicatorColWidth(display);
|
||||
|
||||
if (!portrait) { // uniform 2-line boxes → exact pixel math
|
||||
if (!portrait) { // uniform boxes → exact pixel math
|
||||
const int box = fixed_bh + 1;
|
||||
r.total_px = (long)count * box;
|
||||
r.scroll_px = (long)scroll * box;
|
||||
@@ -278,11 +318,22 @@ class MessagesScreen : public UIScreen {
|
||||
_phase = MSG_PICK;
|
||||
}
|
||||
|
||||
// The keyboard's field label while writing: who it goes to.
|
||||
char _kb_prompt[16];
|
||||
void setComposePrompt() {
|
||||
const char* to = _sel_contact.name;
|
||||
ChannelDetails ch;
|
||||
if (_sending_to_channel && the_mesh.getChannel(_sel_channel_idx, ch)) to = ch.name;
|
||||
snprintf(_kb_prompt, sizeof(_kb_prompt), "%.12s", to);
|
||||
_kb->prompt = _kb_prompt;
|
||||
}
|
||||
|
||||
// Recipient chosen while sharing — open the keyboard with the prepared text.
|
||||
void beginShareCompose(bool channel) {
|
||||
_sending_to_channel = channel;
|
||||
_reply_mode = false;
|
||||
_kb->begin(_share_text);
|
||||
setComposePrompt();
|
||||
_phase = KEYBOARD;
|
||||
}
|
||||
|
||||
@@ -641,6 +692,7 @@ class MessagesScreen : public UIScreen {
|
||||
}
|
||||
_login_mode = true;
|
||||
_kb->begin("", 15); // room/repeater password: max 15 chars
|
||||
_kb->prompt = "Password";
|
||||
_kb->clearPlaceholders(); // {loc}/{time} are for messages, not a password
|
||||
_phase = KEYBOARD;
|
||||
}
|
||||
@@ -1317,24 +1369,55 @@ public:
|
||||
// Portrait e-ink (height > width): variable-height boxes that show the full
|
||||
// wrapped message text. All other displays/orientations: compact 2-line boxes.
|
||||
bool portrait_expand = (display.height() > display.width());
|
||||
// A DM bubble on a compact screen carries no sender line: one line of
|
||||
// text with the delivery / hop marker and the age at its end, growing to
|
||||
// two lines (marker and age ending the second) when the text is longer.
|
||||
// Rooms keep the author line above the text.
|
||||
const bool compact_dm = !is_room && !portrait_expand;
|
||||
const int MAX_VIS_BOXES = 8;
|
||||
int box_ys[MAX_VIS_BOXES], box_hs[MAX_VIS_BOXES], n_vis = 0;
|
||||
|
||||
// The marker + age strip at a compact bubble's end, and its width.
|
||||
struct Tail { int ack_w, age_w, mk_gap, hops, w; char age[6]; };
|
||||
auto tailOf = [&](const DmHistEntry& e) {
|
||||
Tail t;
|
||||
geo::fmtAgeShort(t.age, sizeof(t.age), now_ts, e.timestamp ? e.timestamp : now_ts);
|
||||
t.age_w = t.age[0] ? display.getTextWidth(t.age) + 3 : 0;
|
||||
t.hops = !e.outgoing ? (e.path_len & 63) : 0;
|
||||
t.ack_w = e.outgoing ? (3 + ackGlyphWidth(display, _history.dmEffectiveStatus(e), e.attempt + 1))
|
||||
: (t.hops > 0 ? (3 + miniIconNumberWidth(display, t.hops)) : 0);
|
||||
t.mk_gap = (t.ack_w > 0 && t.age_w > 0) ? 4 : 0;
|
||||
t.w = t.ack_w + t.mk_gap + t.age_w + (t.age_w ? 0 : 3);
|
||||
return t;
|
||||
};
|
||||
auto bodyAt = [&](int idx, char* sb, size_t sbn) -> const char* {
|
||||
int rp = _history.dmHistEntryForContact(_sel_contact.id.pub_key, idx);
|
||||
if (rp < 0) return nullptr;
|
||||
return skipReplyPrefix(dmDisplayParts(_history.dmAtPos(rp), is_room, filtered_name, sb, sbn));
|
||||
};
|
||||
// Height of compact bubble idx at a given scrollbar reserve.
|
||||
auto compactH = [&](int idx, int rsv) {
|
||||
int rp = _history.dmHistEntryForContact(_sel_contact.id.pub_key, idx);
|
||||
if (rp < 0) return lh + 1;
|
||||
char sb[33];
|
||||
const char* body = bodyAt(idx, sb, sizeof(sb));
|
||||
int line_w = bubbleMaxW(display, display.width() - rsv) - 6;
|
||||
return dmSplit(display, body ? body : "", line_w, tailOf(_history.dmAtPos(rp)).w) < 0 ? lh + 1 : 2 * lh + 1;
|
||||
};
|
||||
|
||||
// Fixed-track scrollbar metrics + a stable gutter reserve (decided by a
|
||||
// whole-list fit test, not last frame's visible count) so message boxes
|
||||
// don't reflow their width as messages arrive.
|
||||
HistScroll hs = computeHistScroll(display, portrait_expand, dm_count, _dm_hist_scroll,
|
||||
HistScroll hs = compact_dm
|
||||
? computeHistScrollH(display, dm_count, _dm_hist_scroll, hist_start_y, cby, 1, compactH)
|
||||
: computeHistScroll(display, portrait_expand, dm_count, _dm_hist_scroll,
|
||||
hist_start_y, cby, lh,
|
||||
[&](int idx) -> const char* {
|
||||
int rp = _history.dmHistEntryForContact(_sel_contact.id.pub_key, idx);
|
||||
if (rp < 0) return nullptr;
|
||||
// Must match the per-item body extraction below (dmDisplayParts +
|
||||
// skipReplyPrefix) exactly, or this sizing pass and the real render
|
||||
// pass disagree on wrapped line count for room posts -- previously
|
||||
// this returned the raw "Sender: text" unsplit, wrapping the sender
|
||||
// name in with the body and mis-sizing the box.
|
||||
// pass disagree on wrapped line count for room posts.
|
||||
char tmp_sender[33];
|
||||
return skipReplyPrefix(dmDisplayParts(_history.dmAtPos(rp), is_room, filtered_name,
|
||||
tmp_sender, sizeof(tmp_sender)));
|
||||
return bodyAt(idx, tmp_sender, sizeof(tmp_sender));
|
||||
});
|
||||
int reserve = hs.reserve;
|
||||
{
|
||||
@@ -1347,7 +1430,7 @@ public:
|
||||
const int box_gap = portrait_expand ? 2 : 1;
|
||||
int cur_y = cby - box_gap; // reserve the same gap against compose as between boxes
|
||||
for (int ii = 0; ii < MAX_VIS_BOXES && (_dm_hist_scroll + ii) < dm_count; ii++) {
|
||||
int bh = fixed_bh;
|
||||
int bh = compact_dm ? compactH(_dm_hist_scroll + ii, reserve) : fixed_bh;
|
||||
if (portrait_expand) {
|
||||
int rp = _history.dmHistEntryForContact(_sel_contact.id.pub_key, _dm_hist_scroll + ii);
|
||||
if (rp >= 0) {
|
||||
@@ -1377,7 +1460,54 @@ public:
|
||||
const DmHistEntry& e = _history.dmAtPos(ring_pos);
|
||||
char sender_buf[33];
|
||||
const char* body = skipReplyPrefix(dmDisplayParts(e, is_room, filtered_name, sender_buf, sizeof(sender_buf)));
|
||||
const char* sender = sender_buf;
|
||||
// A DM has two people: the header names the contact and the side a
|
||||
// bubble sits on says who wrote it, so bubbles carry only the delivery
|
||||
// or hop marker and the age. Rooms keep each post's author.
|
||||
const char* sender = is_room ? sender_buf : "";
|
||||
|
||||
if (compact_dm) {
|
||||
Tail t = tailOf(e);
|
||||
int full_avail = display.width() - reserve;
|
||||
int max_w = bubbleMaxW(display, full_avail);
|
||||
int line_w = max_w - 6;
|
||||
int split = dmSplit(display, body, line_w, t.w);
|
||||
char l1[MSG_TEXT_BUF];
|
||||
const char* l2 = nullptr;
|
||||
int body_w;
|
||||
if (split < 0) {
|
||||
int raw = display.getTextWidth(body);
|
||||
body_w = (raw > line_w - t.w ? line_w - t.w : raw) + 6 + t.w;
|
||||
} else {
|
||||
int n = split;
|
||||
while (n > 0 && body[n - 1] == ' ') n--;
|
||||
if (n > (int)sizeof(l1) - 1) n = sizeof(l1) - 1;
|
||||
memcpy(l1, body, n); l1[n] = '\0';
|
||||
l2 = body + split;
|
||||
int w1 = display.getTextWidth(l1), w2 = display.getTextWidth(l2);
|
||||
if (w2 > line_w - t.w) w2 = line_w - t.w;
|
||||
body_w = (w1 > w2 + t.w ? w1 : w2 + t.w) + 6;
|
||||
}
|
||||
BubbleBox box = computeBubbleBox(full_avail, max_w, e.outgoing, 0, body_w);
|
||||
drawHistRowFrame(display, box.x, box.w, y, bh, lh, sel);
|
||||
// Marker and age end the last line of text.
|
||||
const int ty = y + 1 + (l2 ? lh : 0);
|
||||
const int mark_x = box.x + box.w - t.w + 3;
|
||||
if (e.outgoing) drawAckGlyph(display, mark_x, ty, _history.dmEffectiveStatus(e), e.attempt + 1);
|
||||
else if (t.hops > 0) miniIconDrawNumber(display, mark_x, ty, t.hops);
|
||||
if (t.age[0]) { display.setCursor(box.x + box.w - t.age_w, ty); display.print(t.age); }
|
||||
// Only the last line marquees (one shared marquee slot), and not
|
||||
// while the context menu is over it.
|
||||
bool body_marquee = sel && !_ctx_menu.active;
|
||||
int r_body;
|
||||
if (!l2) {
|
||||
r_body = display.drawTextEllipsized(box.x + 3, y + 1, box.w - 6 - t.w, body, body_marquee);
|
||||
} else {
|
||||
display.drawTextEllipsized(box.x + 3, y + 1, box.w - 6, l1, false);
|
||||
r_body = display.drawTextEllipsized(box.x + 3, y + lh + 1, box.w - 6 - t.w, l2, body_marquee);
|
||||
}
|
||||
if (body_marquee && r_body > 0) mq_delay = r_body;
|
||||
continue;
|
||||
}
|
||||
|
||||
// e.timestamp==0 shouldn't happen (storeDMMsg() falls back to receipt
|
||||
// time at write time), but if it ever does, show the receipt time
|
||||
@@ -1399,7 +1529,7 @@ public:
|
||||
: (in_hop_count > 0 ? (3 + miniIconNumberWidth(display, in_hop_count)) : 0);
|
||||
// A little air between a marker and the age, so "12" + "1s" doesn't read as "121s".
|
||||
int mk_gap = (ack_w > 0 && age_w > 0) ? 2 : 0;
|
||||
int header_w = 3 + display.getTextWidth(sender) + ack_w + mk_gap + age_w + 3;
|
||||
int header_w = 3 + (sender[0] ? display.getTextWidth(sender) : -3) + ack_w + mk_gap + age_w + 3;
|
||||
int body_w, nl = 0;
|
||||
if (portrait_expand) {
|
||||
display.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans));
|
||||
@@ -1422,14 +1552,15 @@ public:
|
||||
// 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;
|
||||
if (name_avail < display.getCharWidth()) name_avail = display.getCharWidth();
|
||||
int name_w = display.getTextWidth(sender);
|
||||
int name_w = sender[0] ? display.getTextWidth(sender) : 0;
|
||||
if (name_w > name_avail) name_w = name_avail;
|
||||
display.drawTextEllipsized(box.x + 3, y + 1, name_avail, sender);
|
||||
if (sender[0]) display.drawTextEllipsized(box.x + 3, y + 1, name_avail, sender);
|
||||
const int mark_x = box.x + 3 + (name_w ? name_w + 3 : 0);
|
||||
if (e.outgoing) { // delivery marker after "Me"
|
||||
int gx = box.x + 3 + name_w + 3;
|
||||
int gx = mark_x;
|
||||
drawAckGlyph(display, gx, y + 1, _history.dmEffectiveStatus(e), e.attempt + 1);
|
||||
} else if (in_hop_count > 0) { // hop count after the sender name
|
||||
int gx = box.x + 3 + name_w + 3;
|
||||
int gx = mark_x;
|
||||
miniIconDrawNumber(display, gx, y + 1, in_hop_count);
|
||||
}
|
||||
if (age[0]) { display.setCursor(box.x + box.w - age_w, y + 1); display.print(age); }
|
||||
@@ -1627,7 +1758,7 @@ public:
|
||||
: (in_hop_count > 0 ? (3 + miniIconNumberWidth(display, in_hop_count)) : 0);
|
||||
// A little air between a marker and the age, so "12" + "1s" doesn't read as "121s".
|
||||
int mk_gap = (ack_w > 0 && age_w > 0) ? 2 : 0;
|
||||
int header_w = 3 + display.getTextWidth(sender) + ack_w + mk_gap + age_w + 3;
|
||||
int header_w = 3 + (sender[0] ? display.getTextWidth(sender) : -3) + ack_w + mk_gap + age_w + 3;
|
||||
int body_w, nl = 0;
|
||||
if (portrait_expand) {
|
||||
display.translateUTF8ToBlocks(s_wrap_trans, body, sizeof(s_wrap_trans));
|
||||
@@ -1825,6 +1956,7 @@ public:
|
||||
if (sel == 0) {
|
||||
_login_mode = true;
|
||||
_kb->begin("", 15); // room/repeater password: max 15 chars
|
||||
_kb->prompt = "Password";
|
||||
_kb->clearPlaceholders(); // {loc}/{time} are for messages, not a password
|
||||
_phase = KEYBOARD;
|
||||
} else if (sel == _ctx_pin_idx) {
|
||||
@@ -1910,6 +2042,7 @@ public:
|
||||
// _login_mode handler), whether or not login itself succeeds.
|
||||
_login_mode = true;
|
||||
_kb->begin("", 15); // room/repeater password: max 15 chars
|
||||
_kb->prompt = "Password";
|
||||
_kb->clearPlaceholders();
|
||||
_phase = KEYBOARD;
|
||||
return true;
|
||||
@@ -2377,6 +2510,7 @@ public:
|
||||
if (c == KEY_ENTER) {
|
||||
if (_msg_sel == 0) {
|
||||
_kb->begin(_reply_mode ? _reply_prefix : "");
|
||||
setComposePrompt();
|
||||
kbAddSensorPlaceholders(*_kb, &sensors);
|
||||
_phase = KEYBOARD;
|
||||
return true;
|
||||
|
||||
@@ -497,8 +497,10 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
|
||||
display.setCursor(2, hdr + step); display.print(buf);
|
||||
snprintf(buf, sizeof(buf), "SNR: %.1f dB", e.snr_x4 / 4.0f);
|
||||
display.setCursor(2, hdr + step * 2); display.print(buf);
|
||||
drawSignalBars(display, display.width() - 2, hdr + step * 2, e.snr_x4); // how we hear them
|
||||
snprintf(buf, sizeof(buf), "Rem: %.1f dB", e.remote_snr_x4 / 4.0f);
|
||||
display.setCursor(2, hdr + step * 3); display.print(buf);
|
||||
drawSignalBars(display, display.width() - 2, hdr + step * 3, e.remote_snr_x4); // how they hear us
|
||||
display.setCursor(2, hdr + step * 4);
|
||||
display.print(e.is_known ? "Status: known" : "Status: new");
|
||||
}
|
||||
@@ -631,10 +633,15 @@ public:
|
||||
char empty[24];
|
||||
if (_source == SRC_SCAN) {
|
||||
const char* msg;
|
||||
if (_scanning) msg = "No replies yet"; // fits 128 px; the header already says SCANNING
|
||||
if (_scanning) { // the header already says SCANNING: just show it's listening
|
||||
display.drawTextCentered(display.width() / 2, display.height() / 2 - display.lineStep() / 2, "No replies yet");
|
||||
int r = drawLoadingDots(display, display.width() / 2, display.height() / 2 + display.lineStep() + 2);
|
||||
if (mq_delay <= 0 || r < mq_delay) mq_delay = r;
|
||||
msg = nullptr;
|
||||
}
|
||||
else if (flt) { snprintf(empty, sizeof(empty), "No %s nodes", flt); msg = empty; }
|
||||
else msg = "No nodes found";
|
||||
display.drawTextCentered(display.width() / 2, display.height() / 2, msg);
|
||||
if (msg) display.drawTextCentered(display.width() / 2, display.height() / 2, msg);
|
||||
} else {
|
||||
const char* hint;
|
||||
if (_pick_admin_target && !flt) { snprintf(empty, sizeof(empty), "Nothing to admin"); hint = "Repeaters & rooms"; }
|
||||
@@ -675,8 +682,9 @@ public:
|
||||
|
||||
display.setColor(sel ? DisplayDriver::DARK : DisplayDriver::LIGHT);
|
||||
char right[10];
|
||||
if (_source == SRC_SCAN) {
|
||||
snprintf(right, sizeof(right), "%d", (int)e.rssi);
|
||||
if (_source == SRC_SCAN) { // live scan: how well we hear it, as bars
|
||||
drawSignalBars(display, display.width() - reserve - 2, y, e.snr_x4);
|
||||
right[0] = '\0';
|
||||
} else if (_sort == SORT_TIME) {
|
||||
geo::fmtAgeShort(right, sizeof(right), rtc_clock.getCurrentTime(), e.lastmod);
|
||||
if (!right[0]) snprintf(right, sizeof(right), "?"); // unknown / RTC not synced
|
||||
@@ -684,7 +692,7 @@ public:
|
||||
if (e.dist_km >= 0.0f) geo::fmtDist(right, sizeof(right), e.dist_km, useImperial());
|
||||
else strncpy(right, "?GPS", sizeof(right));
|
||||
}
|
||||
display.drawTextRightAlign(display.width() - reserve - 2, y, right);
|
||||
if (right[0]) display.drawTextRightAlign(display.width() - reserve - 2, y, right);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ struct PopupMenu {
|
||||
// Stops short of the scroll-indicator gutter, like every other list's
|
||||
// selection bar (e.g. drawList's row width - reserve) — otherwise the
|
||||
// bar paints over the indicator's column instead of framing it.
|
||||
display.fillRect(bx + 1, py - 1, bw - 2 - arrow_w, item_h);
|
||||
display.fillSoftRect(bx + 2, py - 1, bw - 4 - arrow_w, item_h);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
@@ -110,7 +110,7 @@ public:
|
||||
}
|
||||
if (sel) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(cx, notes_y, cell_w - 1, cell_h);
|
||||
display.fillSoftRect(cx, notes_y, cell_w - 1, cell_h);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
@@ -192,9 +192,9 @@ public:
|
||||
g->expire();
|
||||
if (!g->sentences || millis() - g->last_ms > 5000) {
|
||||
bool on = _task->getGPSState();
|
||||
display.drawTextCentered(cx, top + lh, on ? "Waiting for GPS..." : "GPS is off");
|
||||
if (!on) display.drawTextCentered(cx, top + 2 * lh + 2, "Home > GPS page");
|
||||
return 1000;
|
||||
display.drawTextCentered(cx, top + lh, on ? "Waiting for GPS" : "GPS is off");
|
||||
if (!on) { display.drawTextCentered(cx, top + 2 * lh + 2, "Home > GPS page"); return 1000; }
|
||||
return drawLoadingDots(display, cx, top + 3 * lh + 4);
|
||||
}
|
||||
if (_signal) renderSignal(display, *g, top);
|
||||
else renderSky(display, *g, top);
|
||||
|
||||
@@ -790,7 +790,7 @@ public:
|
||||
if (res == KeyboardWidget::DONE) {
|
||||
// Entered twice: a typo here would lock the owner out.
|
||||
if (strlen(_kb->buf) < 4) {
|
||||
_kb->pin_prompt = "Min 4";
|
||||
_kb->prompt = "Min 4";
|
||||
} else if (!_lock_pass_first[0]) {
|
||||
strncpy(_lock_pass_first, _kb->buf, sizeof(_lock_pass_first) - 1);
|
||||
_lock_pass_first[sizeof(_lock_pass_first) - 1] = '\0';
|
||||
@@ -853,6 +853,7 @@ public:
|
||||
} else if (sel == 1 && _scope_action_idx >= 1) { // Rename
|
||||
_scope_rename_idx = _scope_action_idx;
|
||||
_kb->begin(sl.name((uint8_t)_scope_action_idx), 23);
|
||||
_kb->prompt = "Scope";
|
||||
_kb->clearPlaceholders();
|
||||
} else if (sel == 2 && _scope_action_idx >= 1) { // Delete -- confirm first
|
||||
_scope_action_menu.beginConfirm("Delete scope?", "Delete");
|
||||
@@ -869,6 +870,7 @@ public:
|
||||
if (_scope_mgmt_sel == sl.totalCount()) { // "+ Add scope"
|
||||
_scope_rename_idx = -1;
|
||||
_kb->begin("", 23);
|
||||
_kb->prompt = "Scope";
|
||||
_kb->clearPlaceholders();
|
||||
} else {
|
||||
_scope_action_idx = _scope_mgmt_sel;
|
||||
@@ -910,6 +912,7 @@ public:
|
||||
switch (_picker.onSelected(_picker.menu.selectedIndex(), p, radioTarget(p))) {
|
||||
case RadioPresetPicker::START_SAVE:
|
||||
_kb->begin("", (int)sizeof(p->user_radio_presets[0].name) - 1);
|
||||
_kb->prompt = "Preset name";
|
||||
_kb->clearPlaceholders(); // {loc}/{time} are for messages, not preset names
|
||||
break;
|
||||
case RadioPresetPicker::APPLIED:
|
||||
@@ -1038,6 +1041,7 @@ public:
|
||||
if (_selected == DEVICE_NAME && p && enter) {
|
||||
_edit_name = true;
|
||||
_kb->begin(the_mesh.getNodeName(), (int)sizeof(p->node_name) - 1);
|
||||
_kb->prompt = "Node name";
|
||||
_kb->clearPlaceholders(); // a device name is literal, not a message
|
||||
return true;
|
||||
}
|
||||
@@ -1140,6 +1144,7 @@ public:
|
||||
// can't overflow it on save.
|
||||
_kb->begin(p ? p->custom_msgs[slot] : "",
|
||||
p ? (int)sizeof(p->custom_msgs[slot]) - 1 : KB_MAX_LEN);
|
||||
_kb->prompt = "Quick msg";
|
||||
kbAddSensorPlaceholders(*_kb, &sensors);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ inline void drawPill(DisplayDriver& display, int x, int w, const char* label, bo
|
||||
int lh = display.getLineHeight();
|
||||
if (active) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(x, 0, w, lh + 1);
|
||||
display.fillSoftRect(x, 0, w, lh + 1);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
@@ -27,96 +27,64 @@ inline void drawPill(DisplayDriver& display, int x, int w, const char* label, bo
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
|
||||
// Truncate `label` to fit within `max_w` pixels of text (no padding). Similar
|
||||
// intent to DisplayDriver::drawTextEllipsized (which can't be reused directly
|
||||
// here: it draws immediately at a fixed x, but a tab pill needs the truncated
|
||||
// width known *before* drawing, to size/centre the pill), but verifies the
|
||||
// fit by re-measuring the actual "text..." candidate at each step, rather
|
||||
// than estimating via a separately-measured ellipsis width -- font kerning
|
||||
// between the last real glyph and the first dot can make a single-pass
|
||||
// estimate overshoot by a px or two, which was enough to visibly spill the
|
||||
// dots into whatever sits just past this tab (a neighbour, a reserved icon
|
||||
// like the context-menu hint). Returns the resulting text's pixel width
|
||||
// (always <= max_w, unless even a bare "..." doesn't fit); writes the
|
||||
// (possibly truncated) text into `out`.
|
||||
inline int ellipsize(DisplayDriver& display, const char* label, int max_w, char* out, size_t out_sz) {
|
||||
strncpy(out, label, out_sz - 1);
|
||||
out[out_sz - 1] = '\0';
|
||||
int w = display.getTextWidth(out);
|
||||
if (w <= max_w) return w;
|
||||
|
||||
int len = (int)strlen(out);
|
||||
char trial[40];
|
||||
while (len > 0) {
|
||||
// Strip an orphaned UTF-8 lead byte before trying this length.
|
||||
while (len > 0 && ((uint8_t)out[len - 1] & 0xC0) == 0xC0) len--;
|
||||
if (len == 0) break;
|
||||
memcpy(trial, out, len);
|
||||
strcpy(trial + len, "...");
|
||||
if (display.getTextWidth(trial) <= max_w) {
|
||||
strncpy(out, trial, out_sz - 1);
|
||||
out[out_sz - 1] = '\0';
|
||||
return display.getTextWidth(out);
|
||||
}
|
||||
len--;
|
||||
// Small solid triangle pointing left (dir < 0) or right, 3 px wide and 5
|
||||
// tall, centred on row y_mid: "more tabs this way".
|
||||
inline void drawEdgeArrow(DisplayDriver& display, int x, int y_mid, int dir) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
for (int c = 0; c < 3; c++) {
|
||||
int h = dir < 0 ? c * 2 + 1 : 5 - c * 2;
|
||||
display.fillRect(x + c, y_mid - h / 2, 1, h);
|
||||
}
|
||||
strncpy(out, "...", out_sz - 1);
|
||||
out[out_sz - 1] = '\0';
|
||||
return display.getTextWidth(out);
|
||||
}
|
||||
|
||||
// Header as a circular tab bar: the active tab sits centred as a filled pill,
|
||||
// neighbours fan out either side and wrap around. A neighbour that doesn't
|
||||
// fully fit in the space remaining on its side is drawn truncated with an
|
||||
// ellipsis instead of being skipped, so the immediate left/right tabs stay
|
||||
// visible (just shortened) rather than vanishing on a narrow display or a
|
||||
// long active label -- only once even an ellipsis wouldn't fit does that side
|
||||
// stop. `right_reserve` keeps any trailing decoration the caller draws
|
||||
// afterwards (a context-menu hint, a counter) clear of the rightmost tab.
|
||||
// Also draws the header separator line.
|
||||
// Header as a circular tab bar: the active tab sits centred as a soft pill and
|
||||
// the neighbours that fit whole fan out either side. When some tabs don't fit,
|
||||
// a small arrow at each edge says there are more (the bar wraps around, so
|
||||
// both ways lead to them) instead of squeezing in cut-off "Sn..." labels.
|
||||
// `right_reserve` keeps any trailing decoration the caller draws afterwards
|
||||
// (a context-menu hint, a counter) clear. Also draws the header separator.
|
||||
inline void draw(DisplayDriver& display, const char* const* labels, int count, int active, int right_reserve = 0) {
|
||||
const int pad = 3, gap = 2;
|
||||
const int min_w = pad * 2 + 6; // smallest pill worth drawing (padding + a couple px of text)
|
||||
char buf[24];
|
||||
const int aw = display.getTextWidth(labels[active]) + pad * 2;
|
||||
const int ax = display.width() / 2 - aw / 2;
|
||||
const int rx_limit = display.width() - right_reserve;
|
||||
|
||||
// Lays the neighbours out (and draws them when `paint`), keeping `edge` px
|
||||
// free at both ends; returns how many tabs are on screen and the free space
|
||||
// left at each end.
|
||||
struct Fit { int shown, left_free, right_free; };
|
||||
auto layout = [&](int edge, bool paint) {
|
||||
int lx = ax - gap, rx = ax + aw + gap, shown = 1;
|
||||
bool lfit = true, rfit = true;
|
||||
for (int k = 1; k <= count / 2 && (lfit || rfit); k++) {
|
||||
int li = (active - k + count) % count;
|
||||
int ri = (active + k) % count;
|
||||
if (lfit) {
|
||||
int w = display.getTextWidth(labels[li]) + pad * 2;
|
||||
if (lx - w >= edge) { if (paint) drawPill(display, lx - w, w, labels[li], false); lx -= w + gap; shown++; }
|
||||
else lfit = false;
|
||||
}
|
||||
// Skip the right side when it lands on the same tab as the left (the
|
||||
// single opposite tab on an even count) so it isn't drawn twice.
|
||||
if (rfit && ri != li) {
|
||||
int w = display.getTextWidth(labels[ri]) + pad * 2;
|
||||
if (rx + w <= rx_limit - edge) { if (paint) drawPill(display, rx, w, labels[ri], false); rx += w + gap; shown++; }
|
||||
else rfit = false;
|
||||
}
|
||||
}
|
||||
return Fit{ shown, lx + gap, rx_limit - rx + gap };
|
||||
};
|
||||
|
||||
int aw = display.getTextWidth(labels[active]) + pad * 2;
|
||||
int ax = display.width() / 2 - aw / 2;
|
||||
drawPill(display, ax, aw, labels[active], true);
|
||||
|
||||
int lx = ax - gap;
|
||||
int rx = ax + aw + gap;
|
||||
int rx_limit = display.width() - right_reserve;
|
||||
bool lfit = true, rfit = true;
|
||||
for (int k = 1; k <= count / 2 && (lfit || rfit); k++) {
|
||||
int li = (active - k + count) % count;
|
||||
int ri = (active + k) % count;
|
||||
if (lfit) {
|
||||
int avail = lx; // space from x=0 to the running left edge
|
||||
if (avail >= min_w) {
|
||||
int tw = ellipsize(display, labels[li], avail - pad * 2, buf, sizeof(buf));
|
||||
int w = tw + pad * 2;
|
||||
// ellipsize() targets max_w but can overshoot it by a px or two (the
|
||||
// ellipsis is re-measured as a whole, so kerning against the last real
|
||||
// character isn't accounted for) -- clamp so the pill can never spill
|
||||
// past its allotted space (into the screen edge, or a reserved icon
|
||||
// on the other side).
|
||||
if (w > avail) w = avail;
|
||||
drawPill(display, lx - w, w, buf, false);
|
||||
lx = lx - w - gap;
|
||||
} else lfit = false;
|
||||
}
|
||||
// Skip the right side when it lands on the same tab as the left (the
|
||||
// single opposite tab on an even count) so it isn't drawn twice.
|
||||
if (rfit && ri != li) {
|
||||
int avail = rx_limit - rx;
|
||||
if (avail >= min_w) {
|
||||
int tw = ellipsize(display, labels[ri], avail - pad * 2, buf, sizeof(buf));
|
||||
int w = tw + pad * 2;
|
||||
if (w > avail) w = avail; // never spill past rx_limit (e.g. into a context-menu icon)
|
||||
drawPill(display, rx, w, buf, false);
|
||||
rx += w + gap;
|
||||
} else rfit = false;
|
||||
}
|
||||
Fit f = layout(0, false);
|
||||
if (f.shown < count && f.left_free < 4 && f.right_free < 4)
|
||||
f = layout(5, true); // no room for an arrow anywhere: give up a tab for them
|
||||
else
|
||||
layout(0, true);
|
||||
if (f.shown < count) { // more tabs off screen: an arrow wherever there's room
|
||||
int y_mid = display.getLineHeight() / 2;
|
||||
if (f.left_free >= 4) drawEdgeArrow(display, 0, y_mid, -1);
|
||||
if (f.right_free >= 4) drawEdgeArrow(display, rx_limit - 3, y_mid, +1);
|
||||
}
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, display.headerH() - display.sepH(), display.width(), display.sepH());
|
||||
|
||||
@@ -52,20 +52,9 @@
|
||||
#define UI_RECENT_LIST_SIZE 4
|
||||
#endif
|
||||
|
||||
// The sim's D-pad + dedicated OK/Enter key behaves like a joystick board
|
||||
// (a short Enter press opens Settings/Tools/Messages -- see the
|
||||
// KEY_ENTER && _page==... handlers below; holding it separately reaches
|
||||
// KEY_CONTEXT_MENU via handleLongPress(), same as a real joystick board's
|
||||
// long-press) -- without SIM_PLATFORM here, this would fall to the
|
||||
// touchscreen-board wording below, which describes a different, and for
|
||||
// this input method simply wrong, interaction.
|
||||
#if UI_HAS_JOYSTICK || defined(SIM_PLATFORM)
|
||||
#define PRESS_LABEL "press Enter"
|
||||
#else
|
||||
#define PRESS_LABEL "long press"
|
||||
#endif
|
||||
|
||||
#include "icons.h"
|
||||
#include "../ui-core/Lettering.h" // boot splash wordmark + lettering (shared with the L2)
|
||||
#include "GfxUtils.h" // gfx::drawLine — connects trail points on the Home map preview
|
||||
|
||||
// Blinking status indicators: on for the first half of a 4 s cycle, but e-ink
|
||||
@@ -74,68 +63,77 @@ static inline bool blinkOn() {
|
||||
return Features::BLINK_INDICATORS ? ((millis() % 4000) < 2000) : true;
|
||||
}
|
||||
|
||||
// Boot splash, as on the L2 (ui-lvgl/Splash.h, same lettering from
|
||||
// ui-core/Lettering.h): the MeshCore wordmark rising into place, "solo" in its
|
||||
// lettering once it has landed, the Solo version, upstream version + build date, and three dots
|
||||
// rising in turn while the device starts. E-ink gets the still final frame.
|
||||
class SplashScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
unsigned long dismiss_after;
|
||||
char _version_info[12];
|
||||
char _solo_ver[12];
|
||||
unsigned long _start, dismiss_after;
|
||||
char _solo_ver[24];
|
||||
char _line2[32];
|
||||
|
||||
// A word in the wordmark's lettering, 1:1, top-left at (x, y).
|
||||
static void drawLettering(DisplayDriver& d, const char* text, int x, int y, int gap) {
|
||||
for (const char* p = text; *p; p++) {
|
||||
char c = (char)tolower((unsigned char)*p);
|
||||
int cw = lettering::charW(c);
|
||||
if (cw < 0) continue;
|
||||
for (int r = 0; r < lettering::LOGO_H; r++)
|
||||
for (int col = 0; col < cw; ) { // runs of inked pixels as one rect
|
||||
if (!lettering::inked(c, col, r)) { col++; continue; }
|
||||
int e = col;
|
||||
while (e + 1 < cw && lettering::inked(c, e + 1, r)) e++;
|
||||
d.fillRect(x + col, y + r, e - col + 1, 1);
|
||||
col = e + 1;
|
||||
}
|
||||
x += cw + gap;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
SplashScreen(UITask* task) : _task(task) {
|
||||
// MeshCore upstream version shown large (e.g. "1.15")
|
||||
strncpy(_version_info, MESHCORE_VERSION, sizeof(_version_info) - 1);
|
||||
_version_info[sizeof(_version_info) - 1] = '\0';
|
||||
|
||||
// Solo firmware version: strip the commit-hash suffix build.sh always
|
||||
// appends as the LAST dash-segment (v1.15-solo.1-abcdef -> v1.15-solo.1).
|
||||
// Must be the last dash, not the first: a tag like v1.21-rc1 has a dash
|
||||
// of its own before the commit hash gets appended.
|
||||
const char *ver = FIRMWARE_VERSION;
|
||||
const char *dash = strrchr(ver, '-');
|
||||
int plen = dash ? (int)(dash - ver) : (int)strlen(ver);
|
||||
if (plen >= (int)sizeof(_solo_ver)) plen = sizeof(_solo_ver) - 1;
|
||||
memcpy(_solo_ver, ver, plen);
|
||||
_solo_ver[plen] = '\0';
|
||||
|
||||
dismiss_after = millis() + BOOT_SCREEN_MILLIS;
|
||||
lettering::soloVersion(_solo_ver, sizeof(_solo_ver));
|
||||
char date[16];
|
||||
lettering::buildDate(date, sizeof(date));
|
||||
#ifdef MESHCORE_VERSION
|
||||
snprintf(_line2, sizeof(_line2), "%s %s", MESHCORE_VERSION, date);
|
||||
#else
|
||||
snprintf(_line2, sizeof(_line2), "%s", date);
|
||||
#endif
|
||||
_start = millis();
|
||||
dismiss_after = _start + BOOT_SCREEN_MILLIS;
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
const bool anim = !display.isEink();
|
||||
const unsigned long t = anim ? millis() - _start : 100000UL; // e-ink: the settled frame
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
const int lh = display.getLineHeight();
|
||||
const int step = display.lineStep();
|
||||
const int cx = display.width() / 2;
|
||||
|
||||
// meshcore logo
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
int logoWidth = 128;
|
||||
int logo_y = 3;
|
||||
display.drawXbm((display.width() - logoWidth) / 2, logo_y, meshcore_logo, logoWidth, 13);
|
||||
// Block from the wordmark down to the second text line, centred above the dots.
|
||||
const int dots_h = 8;
|
||||
const int block_h = lettering::LOGO_H * 2 + 5 + lh * 2 + 2;
|
||||
int top = (display.height() - dots_h - block_h) / 2;
|
||||
if (top < 0) top = 0;
|
||||
|
||||
// version info at sz2
|
||||
int ver_y = logo_y + 13 + 2;
|
||||
display.setTextSize(2);
|
||||
int lh2 = display.getLineHeight();
|
||||
display.drawTextCentered(display.width()/2, ver_y, _version_info);
|
||||
// Wordmark: rises 6 px into place over the first 400 ms (ease-out).
|
||||
int rise = 0;
|
||||
if (t < 400) { int k = 400 - (int)t; rise = (6 * k * k) / (400 * 400); }
|
||||
display.drawXbm((display.width() - lettering::LOGO_W) / 2, top + rise, lettering::LOGO, lettering::LOGO_W, lettering::LOGO_H);
|
||||
|
||||
// build date at sz1, below sz2 version
|
||||
int date_y = ver_y + lh2 + 2;
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width()/2, date_y, FIRMWARE_BUILD_DATE);
|
||||
int y = top + lettering::LOGO_H + 3;
|
||||
const int solo_w = lettering::textW("solo", 2);
|
||||
if (t >= 400) drawLettering(display, "solo", cx - solo_w / 2, y, 2); // once the wordmark has landed
|
||||
y += lettering::LOGO_H + 2;
|
||||
display.drawTextCentered(cx, y, _solo_ver[0] ? _solo_ver : "dev");
|
||||
y += lh + 2;
|
||||
display.drawTextCentered(cx, y, _line2);
|
||||
|
||||
#ifdef FIRMWARE_SOLO_BUILD
|
||||
int solo_y = date_y + step;
|
||||
display.fillRect(0, solo_y - 1, display.width(), lh + 2);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
char solo_label[24];
|
||||
if (_solo_ver[0])
|
||||
snprintf(solo_label, sizeof(solo_label), "Solo %s", _solo_ver);
|
||||
else
|
||||
snprintf(solo_label, sizeof(solo_label), "Solo");
|
||||
display.drawTextCentered(display.width()/2, solo_y, solo_label);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
#endif
|
||||
|
||||
return 1000;
|
||||
drawLoadingDots(display, cx, display.height() - 2); // as the L2's
|
||||
return anim ? 40 : 1000;
|
||||
}
|
||||
|
||||
void poll() override {
|
||||
@@ -722,6 +720,7 @@ public:
|
||||
int render(DisplayDriver& display) override {
|
||||
char tmp[80];
|
||||
int mq_delay = 0; // >0 while a selected row's name is marquee-scrolling
|
||||
int anim_ms = 0; // >0 while a hovering page icon is on screen
|
||||
display.setTextSize(1);
|
||||
const int lh = display.getLineHeight(); // line height at sz1
|
||||
const int step = display.lineStep(); // lh + 2
|
||||
@@ -752,8 +751,19 @@ public:
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
if (_page != LOCK) {
|
||||
// The time, once the clock is set (the node name before that): the
|
||||
// name is what others see, the time is what you glance at here.
|
||||
char filtered_name[sizeof(_node_prefs->node_name)];
|
||||
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
|
||||
uint32_t now_ts = _rtc->getCurrentTime();
|
||||
if (now_ts >= 1000000000UL) {
|
||||
struct tm lt;
|
||||
localTm(now_ts, _node_prefs->tz_offset_hours, lt);
|
||||
int hh = lt.tm_hour;
|
||||
if (_node_prefs->clock_12h) { hh %= 12; if (hh == 0) hh = 12; }
|
||||
snprintf(filtered_name, sizeof(filtered_name), "%d:%02d", hh, lt.tm_min);
|
||||
} else {
|
||||
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
|
||||
}
|
||||
|
||||
// Only show the live-power readout when APC is actually controlling power —
|
||||
// not merely when the pref is set. While repeating APC is suppressed and
|
||||
@@ -791,9 +801,15 @@ public:
|
||||
int x = display.width() / 2 - pitch * (n - 1) / 2;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const MiniIcon* ic = pageIcon(order[i]);
|
||||
if (i == curr_vis) { // the current page: icon knocked out of a soft pill
|
||||
int pw = icon_w + 4;
|
||||
if (pw > pitch - 1) pw = pitch - 1;
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillSoftRect(x - pw / 2, dots_y - pg_half - 1, pw, pg_half * 2 + 3);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
}
|
||||
if (ic) miniIconDrawCentered(display, x, dots_y, *ic);
|
||||
if (i == curr_vis) // underline the current page
|
||||
display.fillRect(x - icon_w / 2, dots_y + pg_half + 1, icon_w, s);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
x += pitch;
|
||||
}
|
||||
}
|
||||
@@ -822,7 +838,8 @@ public:
|
||||
static const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
|
||||
static const char* mo[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
|
||||
snprintf(buf, sizeof(buf),"%s %d %s %d", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon], 1900 + ti->tm_year);
|
||||
display.drawTextCentered(display.width() / 2, date_y, buf);
|
||||
display.setCursor(0, date_y);
|
||||
display.print(buf);
|
||||
|
||||
// Alarm armed: a small bell in the top-left corner. The status bar (and
|
||||
// its bell) is hidden on this page, so signal the armed alarm here. Just
|
||||
@@ -876,6 +893,7 @@ public:
|
||||
struct tm* ti = <
|
||||
char buf[12];
|
||||
bool h12 = _node_prefs && _node_prefs->clock_12h;
|
||||
// Left-aligned on purpose: the rest of the top row is left for status icons.
|
||||
int date_y = drawClockTime(display, 0, ti, h12, /*show_sec*/false);
|
||||
display.setTextSize(1);
|
||||
static const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
|
||||
@@ -961,9 +979,8 @@ public:
|
||||
} else if (_page == HomePage::BLUETOOTH) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(1);
|
||||
display.drawXbm((display.width() - 32) / 2, content_y,
|
||||
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off, 32, 32);
|
||||
const int text_y = content_y + 32 + 3;
|
||||
anim_ms = drawHoverIcon(display, display.width() / 2, content_y, BIG_BLUETOOTH, !_task->isSerialEnabled());
|
||||
const int text_y = content_y + BIG_BLUETOOTH.h + HOVER_GAP;
|
||||
// The pairing PIN is BLE-specific: show it while BLE is on but not yet
|
||||
// bonded. (Gating on a plain isConnected() broke this on dual builds,
|
||||
// where it's hardcoded true.)
|
||||
@@ -972,18 +989,18 @@ public:
|
||||
char pin_buf[16];
|
||||
snprintf(pin_buf, sizeof(pin_buf), "PIN: %d", the_mesh.getBLEPin());
|
||||
display.drawTextCentered(display.width() / 2, text_y, pin_buf);
|
||||
} else if (waiting_for_pair) {
|
||||
} else if (waiting_for_pair) { // the pairing PIN takes the title's place
|
||||
char pin_buf[16];
|
||||
snprintf(pin_buf, sizeof(pin_buf), "PIN: %d", the_mesh.getBLEPin());
|
||||
display.drawTextCentered(display.width() / 2, text_y, pin_buf);
|
||||
display.drawTextCentered(display.width() / 2, text_y + step, "toggle: " PRESS_LABEL);
|
||||
} else {
|
||||
display.drawTextCentered(display.width() / 2, text_y, "toggle: " PRESS_LABEL);
|
||||
// Each icon page carries just its title; Enter acting on it is the same everywhere.
|
||||
display.drawTextCentered(display.width() / 2, text_y, "Bluetooth");
|
||||
}
|
||||
} else if (_page == HomePage::ADVERT) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawXbm((display.width() - 32) / 2, content_y, advert_icon, 32, 32);
|
||||
display.drawTextCentered(display.width() / 2, content_y + 32 + 3, "advert: " PRESS_LABEL);
|
||||
anim_ms = drawHoverIcon(display, display.width() / 2, content_y, BIG_ADVERT);
|
||||
display.drawTextCentered(display.width() / 2, content_y + BIG_ADVERT.h + HOVER_GAP, "Advert");
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
} else if (_page == HomePage::GPS) {
|
||||
LocationProvider* nmea = sensors.getLocationProvider();
|
||||
@@ -1095,8 +1112,8 @@ public:
|
||||
} else if (_page == HomePage::SETTINGS) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width() / 2, content_y, "Settings");
|
||||
display.drawTextCentered(display.width() / 2, content_y + step * 2, PRESS_LABEL " to open");
|
||||
anim_ms = drawHoverIcon(display, display.width() / 2, content_y, BIG_SETTINGS);
|
||||
display.drawTextCentered(display.width() / 2, content_y + BIG_SETTINGS.h + HOVER_GAP, "Settings");
|
||||
} else if (_page == HomePage::MAP) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(1);
|
||||
@@ -1137,19 +1154,24 @@ public:
|
||||
} else if (_page == HomePage::TOOLS) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width() / 2, content_y, "Tools");
|
||||
display.drawTextCentered(display.width() / 2, content_y + step * 2, PRESS_LABEL " to open");
|
||||
anim_ms = drawHoverIcon(display, display.width() / 2, content_y, BIG_TOOLS);
|
||||
display.drawTextCentered(display.width() / 2, content_y + BIG_TOOLS.h + HOVER_GAP, "Tools");
|
||||
} else if (_page == HomePage::QUICK_MSG) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setTextSize(1);
|
||||
display.drawTextCentered(display.width() / 2, content_y, "Messages");
|
||||
const int ix = (display.width() - BIG_MESSAGES.w) / 2;
|
||||
anim_ms = drawHoverIcon(display, display.width() / 2, content_y, BIG_MESSAGES);
|
||||
// Unread count as the usual pill on the bubble's top-right corner.
|
||||
int total_unread = _task->getDMUnreadTotal() + _task->getChannelUnreadCount() + _task->getRoomUnreadCount();
|
||||
if (total_unread > 0) {
|
||||
char badge[20];
|
||||
snprintf(badge, sizeof(badge), _task->getAnyUnreadOverflow() ? "%d+ unread" : "%d unread", total_unread);
|
||||
display.drawTextCentered(display.width() / 2, content_y + step, badge);
|
||||
int bw = display.unreadBadgeWidth(total_unread, _task->getAnyUnreadOverflow());
|
||||
int bx = ix + BIG_MESSAGES.w + bw / 2;
|
||||
display.setColor(DisplayDriver::DARK); // a dark ring so it reads over the bubble's border
|
||||
const int by = content_y - hoverLift(display); // rides on the hovering bubble
|
||||
display.fillRect(bx - bw - 1, by + 1, bw + 2, lh + 2);
|
||||
display.drawUnreadBadge(bx, by + 2, total_unread, false, _task->getAnyUnreadOverflow());
|
||||
}
|
||||
display.drawTextCentered(display.width() / 2, content_y + step * 2, PRESS_LABEL " to open");
|
||||
display.drawTextCentered(display.width() / 2, content_y + BIG_MESSAGES.h + HOVER_GAP, "Messages");
|
||||
} else if (_page == HomePage::FAVOURITES) {
|
||||
// Grid of pinned contacts. Layout transposes to current orientation:
|
||||
// landscape → 3×2, portrait → 2×3. Selected tile inverts via drawSelectionRow.
|
||||
@@ -1247,20 +1269,11 @@ public:
|
||||
if (_shutdown_init) {
|
||||
display.drawTextCentered(display.width() / 2, content_y + step, "hibernating...");
|
||||
} else {
|
||||
display.drawXbm((display.width() - 32) / 2, content_y, power_icon, 32, 32);
|
||||
const int text_y = content_y + 32 + 3;
|
||||
anim_ms = drawHoverIcon(display, display.width() / 2, content_y, BIG_POWER);
|
||||
const int text_y = content_y + BIG_POWER.h + HOVER_GAP;
|
||||
const int lh1 = display.getLineHeight();
|
||||
if (text_y + lh1 <= display.height()) {
|
||||
char hib_hint[32];
|
||||
snprintf(hib_hint, sizeof(hib_hint), "hibernate:%s", PRESS_LABEL);
|
||||
if (display.getTextWidth(hib_hint) < display.width()) {
|
||||
display.drawTextCentered(display.width() / 2, text_y, hib_hint);
|
||||
} else {
|
||||
display.drawTextCentered(display.width() / 2, text_y, "hibernate:");
|
||||
if (text_y + step + lh1 <= display.height())
|
||||
display.drawTextCentered(display.width() / 2, text_y + step, PRESS_LABEL);
|
||||
}
|
||||
}
|
||||
if (text_y + lh1 <= display.height())
|
||||
display.drawTextCentered(display.width() / 2, text_y, "Hibernate");
|
||||
}
|
||||
}
|
||||
bool auto_adv = _node_prefs && _node_prefs->advert_auto_interval_sec > 0;
|
||||
@@ -1281,6 +1294,7 @@ public:
|
||||
return (mq_delay > 0 && mq_delay < ret) ? mq_delay : ret;
|
||||
}
|
||||
int ret = need_blink ? 1000 : 5000;
|
||||
if (anim_ms > 0 && anim_ms < ret) ret = anim_ms;
|
||||
return (mq_delay > 0 && mq_delay < ret) ? mq_delay : ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <math.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().
|
||||
// (e.g. landscape e-ink renders text at 2×). Distinct from the big page icons
|
||||
// further down, which are fixed-size 32 px glyphs drawn via bigIconDraw().
|
||||
//
|
||||
// To add a mini-icon:
|
||||
// 1. Draw it as ASCII-art rows, one string per row (width ≤ 8): any char
|
||||
@@ -500,7 +501,7 @@ MINI_ICON(ICON_SCROLL_DOWN, 5, // ▼
|
||||
// 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
|
||||
return 3 * miniIconScale(d) + 2; // thumb (3*scale) + 2px gap
|
||||
}
|
||||
inline int scrollIndicatorReserve(DisplayDriver& d, int total, int visible) {
|
||||
return (total > visible) ? scrollIndicatorColWidth(d) : 0;
|
||||
@@ -534,37 +535,23 @@ inline void drawScrollIndicatorPx(DisplayDriver& d, int right_x, int top_y, int
|
||||
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
|
||||
// A dotted 1 px track down the middle of a 3*scale column and a soft-cornered
|
||||
// thumb sized to the share of the list on screen. The column is narrower
|
||||
// than the old arrow-capped one, so rows keep 2 px more for text.
|
||||
const int s = miniIconScale(d);
|
||||
const int bar_w = 3 * s;
|
||||
const int x = right_x - bar_w;
|
||||
const int mid = x + bar_w / 2;
|
||||
|
||||
// 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;
|
||||
int thumb_h = (int)((long)track_h * view_px / total_px);
|
||||
if (thumb_h < 4 * s) thumb_h = 4 * s;
|
||||
if (thumb_h > track_h) thumb_h = track_h;
|
||||
const int thumb_y = top_y + (int)((long)(track_h - thumb_h) * scroll_px / span);
|
||||
|
||||
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);
|
||||
for (int y = top_y; y < top_y + track_h; y += 2)
|
||||
if (y < thumb_y - 1 || y > thumb_y + thumb_h) d.fillRect(mid, y, 1, 1);
|
||||
d.fillSoftRect(x, thumb_y, bar_w, thumb_h);
|
||||
}
|
||||
|
||||
// Convenience overload anchored to the screen's right edge — the common case
|
||||
@@ -613,6 +600,52 @@ inline int drawList(DisplayDriver& d, int total, int sel, int& scroll, RenderRow
|
||||
return visible;
|
||||
}
|
||||
|
||||
// Signal strength as four rising bars, from a LoRa SNR in quarter-dB (the
|
||||
// unit packets carry). Bars that aren't lit leave a 1 px stub so the scale
|
||||
// stays readable. Bottom-aligned to a text row at y; right edge at x_right.
|
||||
// Returns the width used. Draws in the current ink.
|
||||
inline int signalBarsFromSnr(int snr_x4) {
|
||||
int snr = snr_x4 / 4;
|
||||
return snr >= 5 ? 4 : snr >= 0 ? 3 : snr >= -7 ? 2 : snr >= -13 ? 1 : 0;
|
||||
}
|
||||
inline int signalBarsWidth(DisplayDriver& d) { int s = miniIconScale(d); return 4 * 2 * s + 3 * s; }
|
||||
inline int drawSignalBars(DisplayDriver& d, int x_right, int y, int snr_x4) {
|
||||
const int s = miniIconScale(d);
|
||||
const int bw = 2 * s, gap = s, w = signalBarsWidth(d);
|
||||
const int base = y + d.getLineHeight() - 2; // sits on the text baseline
|
||||
const int lit = signalBarsFromSnr(snr_x4);
|
||||
int x = x_right - w;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int h = (i + 1) * 2 * s;
|
||||
if (i < lit) d.fillRect(x, base - h + 1, bw, h);
|
||||
else d.fillRect(x, base, bw, s);
|
||||
x += bw + gap;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
// Three dots rising in turn, each a beat after the last: the "working on it"
|
||||
// mark shared by the boot splash and every screen waiting on the radio or GPS.
|
||||
// Centred on cx with their resting bottom at y_bottom. Returns the redraw
|
||||
// delay the animation needs; e-ink draws them still and asks for none sooner.
|
||||
inline int drawLoadingDots(DisplayDriver& d, int cx, int y_bottom) {
|
||||
const int s = miniIconScale(d);
|
||||
const int dot = 3 * s, pitch = 9 * s, lift_max = 3 * s;
|
||||
const bool anim = !d.isEink();
|
||||
const unsigned long t = millis();
|
||||
d.setColor(DisplayDriver::LIGHT);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int lift = 0;
|
||||
if (anim) {
|
||||
long ph = (long)((t + 940UL - 140UL * i) % 940UL);
|
||||
if (ph < 260) lift = (int)(lift_max * ph / 260);
|
||||
else if (ph < 520) lift = (int)(lift_max * (520 - ph) / 260);
|
||||
}
|
||||
d.fillRect(cx + (i - 1) * pitch - dot / 2, y_bottom - dot - lift, dot, dot);
|
||||
}
|
||||
return anim ? 40 : 1000;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -623,11 +656,10 @@ 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) ─────────────────────────────
|
||||
// ── Big ASCII-art icons ─────────────────────────────
|
||||
// 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).
|
||||
// wide: one uint32_t per row, readable in source. Drawn at 1× (page icons
|
||||
// aren't font-scaled).
|
||||
//
|
||||
// To add one:
|
||||
// BIG_ICON(MY_ICON, 16,
|
||||
@@ -651,126 +683,264 @@ struct BigIcon { uint8_t w, h; const uint32_t* rows; };
|
||||
(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);
|
||||
// `dim` draws only every other pixel (50% checker): the icon's "off" state.
|
||||
inline void bigIconDraw(DisplayDriver& d, int x, int y, const BigIcon& ic, bool dim = false) {
|
||||
for (int r = 0; r < ic.h; r++) {
|
||||
if (dim) {
|
||||
for (int c = (r + x + y) & 1; c < ic.w; c += 2)
|
||||
if (ic.rows[r] & (1u << c)) d.fillRect(x + c, y + r, 1, 1);
|
||||
continue;
|
||||
}
|
||||
for (int c = 0; c < ic.w; ) { // runs of set bits as one rect
|
||||
if (!(ic.rows[r] & (1u << c))) { c++; continue; }
|
||||
int e = c;
|
||||
while (e + 1 < ic.w && (ic.rows[r] & (1u << (e + 1)))) e++;
|
||||
d.fillRect(x + c, y + r, e - c + 1, 1);
|
||||
c = e + 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,
|
||||
};
|
||||
// Home page icons, 32x32, one family: 3 px strokes, rounded ends. Drawn
|
||||
// through drawHoverIcon() below. Rasterised from vector shapes, so keep any
|
||||
// redraw to the same stroke width rather than touching single pixels.
|
||||
// Bluetooth rune
|
||||
BIG_ICON(BIG_BLUETOOTH, 32,
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("..............##................"),
|
||||
packRow32(".............####..............."),
|
||||
packRow32(".............#####.............."),
|
||||
packRow32(".............######............."),
|
||||
packRow32(".............#######............"),
|
||||
packRow32(".............#########.........."),
|
||||
packRow32(".............####.#####........."),
|
||||
packRow32("........##...####..####........."),
|
||||
packRow32(".......####..####..####........."),
|
||||
packRow32("........####.####.#####........."),
|
||||
packRow32(".........############..........."),
|
||||
packRow32("..........##########............"),
|
||||
packRow32("...........########............."),
|
||||
packRow32("............######.............."),
|
||||
packRow32("............######.............."),
|
||||
packRow32("...........########............."),
|
||||
packRow32("..........##########............"),
|
||||
packRow32(".........############..........."),
|
||||
packRow32("........####.####.#####........."),
|
||||
packRow32(".......####..####..####........."),
|
||||
packRow32("........##...####..####........."),
|
||||
packRow32(".............####.#####........."),
|
||||
packRow32(".............#########.........."),
|
||||
packRow32(".............#######............"),
|
||||
packRow32(".............######............."),
|
||||
packRow32(".............#####.............."),
|
||||
packRow32(".............####..............."),
|
||||
packRow32("..............##................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"));
|
||||
// advert: a dot between two pairs of arcs
|
||||
BIG_ICON(BIG_ADVERT, 32,
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("....#......................#...."),
|
||||
packRow32("...####..................####..."),
|
||||
packRow32("...###....................###..."),
|
||||
packRow32("..###......................###.."),
|
||||
packRow32("..###...##............##...###.."),
|
||||
packRow32(".###...####..........####...###."),
|
||||
packRow32(".###...###.....##.....###...###."),
|
||||
packRow32(".###..###....######....###..###."),
|
||||
packRow32(".##...###....######....###...##."),
|
||||
packRow32(".##...###...########...###...##."),
|
||||
packRow32(".##...###...########...###...##."),
|
||||
packRow32(".##...###....######....###...##."),
|
||||
packRow32(".###..###....######....###..###."),
|
||||
packRow32(".###...###.....##.....###...###."),
|
||||
packRow32(".###...####..........####...###."),
|
||||
packRow32("..###...##............##...###.."),
|
||||
packRow32("..###......................###.."),
|
||||
packRow32("...###....................###..."),
|
||||
packRow32("...####..................####..."),
|
||||
packRow32("....#......................#...."),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"));
|
||||
// power (Hibernate)
|
||||
BIG_ICON(BIG_POWER, 32,
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("...............##..............."),
|
||||
packRow32("..............####.............."),
|
||||
packRow32("..............####.............."),
|
||||
packRow32("..............####.............."),
|
||||
packRow32("..............####.............."),
|
||||
packRow32("..............####.............."),
|
||||
packRow32(".......##.....####.....##......."),
|
||||
packRow32("......####....####....####......"),
|
||||
packRow32(".....####.....####.....####....."),
|
||||
packRow32("....####......####......####...."),
|
||||
packRow32("....###.......####.......###...."),
|
||||
packRow32("....###.......####.......###...."),
|
||||
packRow32("...###........####........###..."),
|
||||
packRow32("...###.........##.........###..."),
|
||||
packRow32("...###....................###..."),
|
||||
packRow32("...###....................###..."),
|
||||
packRow32("...###....................###..."),
|
||||
packRow32("...###....................###..."),
|
||||
packRow32("...###....................###..."),
|
||||
packRow32("....###..................###...."),
|
||||
packRow32("....###..................###...."),
|
||||
packRow32("....####................####...."),
|
||||
packRow32(".....####..............####....."),
|
||||
packRow32("......####............####......"),
|
||||
packRow32(".......#####........#####......."),
|
||||
packRow32("........################........"),
|
||||
packRow32(".........##############........."),
|
||||
packRow32("...........##########..........."),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"));
|
||||
// gear
|
||||
BIG_ICON(BIG_SETTINGS, 32,
|
||||
packRow32("................................"),
|
||||
packRow32(".............######............."),
|
||||
packRow32(".............######............."),
|
||||
packRow32(".............######............."),
|
||||
packRow32(".......##....######....##......."),
|
||||
packRow32("......####...######...####......"),
|
||||
packRow32(".....######################....."),
|
||||
packRow32("....########################...."),
|
||||
packRow32("....########################...."),
|
||||
packRow32(".....######################....."),
|
||||
packRow32("......####################......"),
|
||||
packRow32("......#######......#######......"),
|
||||
packRow32("......######........######......"),
|
||||
packRow32(".##########..........##########."),
|
||||
packRow32(".##########..........##########."),
|
||||
packRow32(".##########..........##########."),
|
||||
packRow32(".##########..........##########."),
|
||||
packRow32(".##########..........##########."),
|
||||
packRow32(".##########..........##########."),
|
||||
packRow32("......######........######......"),
|
||||
packRow32("......#######......#######......"),
|
||||
packRow32("......####################......"),
|
||||
packRow32(".....######################....."),
|
||||
packRow32("....########################...."),
|
||||
packRow32("....########################...."),
|
||||
packRow32(".....######################....."),
|
||||
packRow32("......####...######...####......"),
|
||||
packRow32(".......##....######....##......."),
|
||||
packRow32(".............######............."),
|
||||
packRow32(".............######............."),
|
||||
packRow32(".............######............."),
|
||||
packRow32("................................"));
|
||||
// wrench
|
||||
BIG_ICON(BIG_TOOLS, 32,
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32(".....................##........."),
|
||||
packRow32("..................#######......."),
|
||||
packRow32(".................#######........"),
|
||||
packRow32("................#######........."),
|
||||
packRow32("...............#######.........."),
|
||||
packRow32("...............######.......#..."),
|
||||
packRow32("...............#####.......##..."),
|
||||
packRow32("..............#######.....####.."),
|
||||
packRow32("..............########...#####.."),
|
||||
packRow32("...............########.#####..."),
|
||||
packRow32("...............##############..."),
|
||||
packRow32("...............##############..."),
|
||||
packRow32("..............##############...."),
|
||||
packRow32(".............##############....."),
|
||||
packRow32("............##############......"),
|
||||
packRow32("...........#######...##........."),
|
||||
packRow32("..........#######..............."),
|
||||
packRow32(".........#######................"),
|
||||
packRow32("........#######................."),
|
||||
packRow32(".......#######.................."),
|
||||
packRow32("......#######..................."),
|
||||
packRow32(".....#######...................."),
|
||||
packRow32("....#######....................."),
|
||||
packRow32("....######......................"),
|
||||
packRow32("....#####......................."),
|
||||
packRow32(".....###........................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"));
|
||||
// chat bubble with the loading-dot trio in it
|
||||
BIG_ICON(BIG_MESSAGES, 32,
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("......####################......"),
|
||||
packRow32("....########################...."),
|
||||
packRow32("....########################...."),
|
||||
packRow32("...####..................####..."),
|
||||
packRow32("...###....................###..."),
|
||||
packRow32("...###....................###..."),
|
||||
packRow32("...###....................###..."),
|
||||
packRow32("...###..####..####..####..###..."),
|
||||
packRow32("...###..####..####..####..###..."),
|
||||
packRow32("...###..####..####..####..###..."),
|
||||
packRow32("...###....................###..."),
|
||||
packRow32("...###....................###..."),
|
||||
packRow32("...###....................###..."),
|
||||
packRow32("...####..................####..."),
|
||||
packRow32("....########################...."),
|
||||
packRow32("....########################...."),
|
||||
packRow32("......####################......"),
|
||||
packRow32("........#####..................."),
|
||||
packRow32("........####...................."),
|
||||
packRow32("........###....................."),
|
||||
packRow32("........##......................"),
|
||||
packRow32("........#......................."),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"),
|
||||
packRow32("................................"));
|
||||
|
||||
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,
|
||||
};
|
||||
// A Home page icon hovering over its spot: it bobs up to 2 px on a slow
|
||||
// cosine while a dithered blob below it, its shadow, shrinks as it rises and
|
||||
// spreads as it sinks. Centred on cx in the 32 px box at y: the glyphs leave
|
||||
// a row or two blank at the top, so the icon rests 1 px high and the shadow
|
||||
// fits in the rows the old static icon left above its title. Put the title at
|
||||
// y + h + HOVER_GAP. E-ink gets the resting frame. Returns ms until the next
|
||||
// frame is due.
|
||||
constexpr int HOVER_GAP = 3;
|
||||
// Where the bob is now: 0 = resting, 1 = top. Anything riding on the icon (a
|
||||
// badge) lifts by hoverLift() so it moves with it.
|
||||
inline float hoverPhase(DisplayDriver& d) {
|
||||
if (d.isEink()) return 0;
|
||||
const unsigned long period = 2400;
|
||||
return (1.0f - cosf(6.2831853f * (float)(millis() % period) / period)) * 0.5f;
|
||||
}
|
||||
inline int hoverLift(DisplayDriver& d) { return 1 + (int)(2 * hoverPhase(d) + 0.5f); }
|
||||
inline int drawHoverIcon(DisplayDriver& d, int cx, int y, const BigIcon& ic, bool dim = false) {
|
||||
const bool anim = !d.isEink();
|
||||
const float up = hoverPhase(d);
|
||||
d.setColor(DisplayDriver::LIGHT);
|
||||
bigIconDraw(d, cx - ic.w / 2, y - 1 - (int)(2 * up + 0.5f), ic, dim);
|
||||
// Shadow: three rows, a checker oval with a solid core that fades as it rises.
|
||||
const int w = (int)(28 - 10 * up + 0.5f), core = (int)(w * 0.5f * (1 - up) + 0.5f);
|
||||
const int sy = y + ic.h - 1;
|
||||
for (int r = 0; r < 3; r++) {
|
||||
const int rw = r == 1 ? w : w - 8, x0 = cx - rw / 2;
|
||||
for (int x = x0; x < x0 + rw; x++)
|
||||
if (((x + sy + r) & 1) == 0 || (r == 1 && 2 * (x - cx) + 1 < core && 2 * (cx - x) - 1 < core))
|
||||
d.fillRect(x, sy + r, 1, 1);
|
||||
}
|
||||
return anim ? 60 : 1000;
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -188,7 +188,8 @@ public:
|
||||
void drawSelectionRow(int x, int y, int w, int h, bool sel) {
|
||||
setColor(LIGHT);
|
||||
if (sel) {
|
||||
fillRect(x, y, w, h);
|
||||
if (w > 2 && h > 2) fillSoftRect(x, y, w, h); // soft corners, like every box
|
||||
else fillRect(x, y, w, h);
|
||||
setColor(DARK);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user