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:
Jakub
2026-10-01 22:59:44 +02:00
co-authored by Claude Opus 5.5
parent 1580de53f5
commit 608231ed6b
18 changed files with 877 additions and 500 deletions
+5 -121
View File
@@ -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() {