mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 19:02:00 +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:
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user