feat(l1): one soft-cornered frame for popups, alerts and bubbles; small screen fixes

DisplayDriver gets drawSoftRect / fillSoftRect / drawPanel (the unread badge's
corner knockout; a panel also clears a 1 px moat so lines below stop short of
its border). Used by alerts, popups, the accent picker, the lock hint, chat
bubbles and the send button.

Fixes: no "Msg:" toast for a message landing in the open thread; Nodes scan
text fits the screen; the PIN pad skips the blanks beside 0; Auto-Advert is a
plain value row; the Admin picker says what it needs when empty; the GPS page
shows "-" instead of 999 before a first fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-01 22:13:11 +02:00
co-authored by Claude Opus 5.5
parent a6612ee2b0
commit 1580de53f5
7 changed files with 72 additions and 49 deletions
@@ -22,31 +22,22 @@ public:
void onShow() override { _dirty = false; }
// One value row, drawn like every other settings list (LEFT/RIGHT or Enter
// cycles, Back saves) instead of a screen of its own with typed hints.
int render(DisplayDriver& display) override {
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
int label_y = display.listStart();
int bar_y = label_y + display.lineStep();
int bar_h = display.lineStep();
int tip_y = bar_y + bar_h + 4;
display.drawCenteredHeader("AUTO-ADVERT");
display.setCursor(2, label_y);
display.print("Interval:");
int idx = currentIdx();
display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, bar_y, display.width(), bar_h);
display.setColor(DisplayDriver::DARK);
display.drawTextCentered(display.width() / 2, bar_y + 1, OPT_LABELS[idx]);
display.setColor(DisplayDriver::LIGHT);
display.setCursor(2, tip_y);
display.print("< > to change");
display.setCursor(2, tip_y + display.lineStep());
display.print("[Esc] to save");
return 500;
const int valx = display.width() / 2 + 6;
int sel = 0, scroll = 0;
drawList(display, 1, sel, scroll, [&](int, int y, bool is_sel, int reserve) {
drawRowSelection(display, y, is_sel, reserve);
display.setCursor(4, y);
display.print("Interval");
display.setCursor(valx, y);
display.print(OPT_LABELS[currentIdx()]);
});
return 5000;
}
bool handleInput(char c) override {
@@ -801,10 +801,7 @@ struct KeyboardWidget {
int bx = (display.width() - bw) / 2;
int by = compact_ui ? (chars_y + 2 * lh + 2) : (chars_y + row * cell_h - 1);
int bh = compact_ui ? (lh + 2) : (cell_h + 1);
display.setColor(DisplayDriver::DARK);
display.fillRect(bx, by, bw, bh);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(bx, by, bw, bh);
display.drawPanel(bx, by, bw, bh);
for (int i = 0; i < n; i++) {
char one[5]; kbUtf8CharAt(group, i, one);
char shown[5]; kbApplyCapsUtf8(one, caps, shown, sizeof(shown));
@@ -971,22 +968,27 @@ struct KeyboardWidget {
}
return NONE;
}
// The blank cells either side of 0 are never a stop: moving up/down
// onto one lands on 0, moving sideways steps over it.
auto blank = [&]() { return row < rows && KB_PIN_DIGITS[row][col] == ' '; };
if (c == KEY_UP) { // Navigation
row = (row > 0) ? row - 1 : rows;
if (blank()) col = 1;
return NONE;
}
if (c == KEY_DOWN) {
row = (row < rows) ? row + 1 : 0;
if (blank()) col = 1;
return NONE;
}
if (c == KEY_LEFT) {
int max_col = (row == rows) ? KB_PIN_SPECIAL - 1 : cols - 1;
col = (col > 0) ? col - 1 : max_col;
do { col = (col > 0) ? col - 1 : max_col; } while (blank());
return NONE;
}
if (c == KEY_RIGHT) {
int max_col = (row == rows) ? KB_PIN_SPECIAL - 1 : cols - 1;
col = (col < max_col) ? col + 1 : 0;
do { col = (col < max_col) ? col + 1 : 0; } while (blank());
return NONE;
}
if (c == KEY_ENTER) {
@@ -459,10 +459,10 @@ class MessagesScreen : public UIScreen {
static void drawHistRowFrame(DisplayDriver& d, int box_x, int box_w, int y, int bh, int lh, bool sel) {
d.setColor(DisplayDriver::LIGHT);
if (sel) {
d.fillRect(box_x, y, box_w, bh);
d.fillSoftRect(box_x, y, box_w, bh);
d.setColor(DisplayDriver::DARK);
} else {
d.drawRect(box_x, y, box_w, bh);
d.drawSoftRect(box_x, y, box_w, bh);
}
}
@@ -519,8 +519,8 @@ class MessagesScreen : public UIScreen {
int bw = ctw + 4;
int bx = d.width() - bw;
d.setColor(DisplayDriver::LIGHT);
if (sel) { d.fillRect(bx, cby, bw, lh); d.setColor(DisplayDriver::DARK); }
else d.drawRect(bx, cby, bw, lh);
if (sel) { d.fillSoftRect(bx, cby, bw, lh); d.setColor(DisplayDriver::DARK); }
else d.drawSoftRect(bx, cby, bw, lh);
d.setCursor(bx + 2, cby);
d.print(ctxt);
d.setColor(DisplayDriver::LIGHT);
@@ -631,14 +631,15 @@ public:
char empty[24];
if (_source == SRC_SCAN) {
const char* msg;
if (_scanning) msg = "Waiting for replies...";
if (_scanning) msg = "No replies yet"; // fits 128 px; the header already says SCANNING
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);
} else {
const char* hint;
if (flt) { snprintf(empty, sizeof(empty), "No %s contacts", flt); hint = "[<>] change filter"; }
else { snprintf(empty, sizeof(empty), "No contacts found"); hint = "[Enter]=Discover"; }
if (_pick_admin_target && !flt) { snprintf(empty, sizeof(empty), "Nothing to admin"); hint = "Repeaters & rooms"; }
else if (flt) { snprintf(empty, sizeof(empty), "No %s contacts", flt); hint = "[<>] change filter"; }
else { snprintf(empty, sizeof(empty), "No contacts found"); hint = "[Enter]=Discover"; }
display.drawTextCentered(display.width() / 2, display.height() / 2 - display.lineStep() / 2, empty);
display.drawTextCentered(display.width() / 2, display.height() / 2 + display.lineStep() / 2, hint);
}
+1 -4
View File
@@ -149,10 +149,7 @@ struct PopupMenu {
if (bx < margin) bx = margin;
if (by < margin) by = margin;
display.setColor(DisplayDriver::DARK);
display.fillRect(bx, by, bw, bh);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(bx, by, bw, bh);
display.drawPanel(bx, by, bw, bh);
if (_title) display.drawTextEllipsized(bx + pad, by + 1, bw - pad * 2, _title);
display.fillRect(bx, by + lh + 2, bw, sh); // separator just under the title; gap follows
+16 -11
View File
@@ -932,9 +932,7 @@ public:
const int hy = display.height() - lk_lh - p * 2;
const int hw = display.getTextWidth(hint);
const int hx = (display.width() - hw) / 2;
display.setColor(DisplayDriver::LIGHT);
display.fillRect(hx - p, hy - p, hw + p*2, lk_lh + p*2);
display.setColor(DisplayDriver::DARK);
display.drawPanel(hx - p, hy - p, hw + p*2, lk_lh + p*2);
display.setCursor(hx, hy);
display.print(hint);
} else if (_page == HomePage::RADIO) {
@@ -1015,7 +1013,10 @@ public:
display.drawTextRightAlign(display.width()-1, y, buf);
y += step;
display.drawTextLeftAlign(0, y, "pos");
snprintf(buf, sizeof(buf),"%.4f %.4f",
// The driver reports 999 deg until a first fix: show a dash, not the
// placeholder. After a fix the last known position stays.
if (labs(nmea->getLatitude()) > 90000000L) strcpy(buf, "-");
else snprintf(buf, sizeof(buf),"%.4f %.4f",
nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.);
display.drawTextRightAlign(display.width()-1, y, buf);
y += step;
@@ -1854,9 +1855,16 @@ void UITask::onMessageArrived(const UiEvent& ev) {
}
if (ev.kind == UIEventType::channelMessage) _last_notif_ch_idx = ev.idx;
char alert_buf[80];
snprintf(alert_buf, sizeof(alert_buf), "Msg: %.20s", ev.text);
showAlert(alert_buf, 3000);
// No toast when the message lands in the thread that is open on screen:
// it already shows up there, and the box would only cover it.
bool in_view = curr == messages_screen && _display && _display->isOn() && !_locked &&
((ev.kind == UIEventType::contactMessage && ev.flag && isViewingDM(ev.key)) ||
(ev.kind == UIEventType::channelMessage && ev.idx >= 0 && isViewingChannel((uint8_t)ev.idx)));
if (!in_view) {
char alert_buf[80];
snprintf(alert_buf, sizeof(alert_buf), "Msg: %.20s", ev.text);
showAlert(alert_buf, 3000);
}
if (_display != NULL && !_locked) {
bool wake_disabled = _node_prefs && (_node_prefs->msg_wake_screen_off || inQuietHours(*_node_prefs, rtc_clock.getCurrentTime()));
@@ -1909,10 +1917,7 @@ void UITask::renderAlertOverlay() {
if (nl < 1) nl = 1;
int box_h = nl * lh + pad * 2;
int box_y = (_display->height() - box_h) / 2;
_display->setColor(DisplayDriver::DARK);
_display->fillRect(box_x, box_y, box_w, box_h);
_display->setColor(DisplayDriver::LIGHT);
_display->drawRect(box_x, box_y, box_w, box_h);
_display->drawPanel(box_x, box_y, box_w, box_h);
for (int i = 0; i < nl; i++)
_display->drawTextCentered(_display->width() / 2, box_y + pad + i * lh, s_wrap_lines[i]);
}
+27
View File
@@ -193,6 +193,33 @@ public:
}
}
// Soft-cornered boxes: the same corner-pixel knockout as drawUnreadBadge(),
// shared by bubbles, popups, alerts and hints so every box reads alike.
// Outline only, corners left out; line thickness follows sepH().
void drawSoftRect(int x, int y, int w, int h) {
int t = sepH();
fillRect(x + 1, y, w - 2, t);
fillRect(x + 1, y + h - t, w - 2, t);
fillRect(x, y + 1, t, h - 2);
fillRect(x + w - t, y + 1, t, h - 2);
}
// Filled in the current colour; the four corner pixels aren't painted, so
// they keep whatever is under them.
void fillSoftRect(int x, int y, int w, int h) {
fillRect(x + 1, y, w - 2, h);
fillRect(x, y + 1, 1, h - 2);
fillRect(x + w - 1, y + 1, 1, h - 2);
}
// A box floating over the screen (alert, popup, hint): clears a 1 px moat
// around it so lines of the view below stop short of the border instead of
// running into it, fills it dark and draws the soft border. Leaves ink LIGHT.
void drawPanel(int x, int y, int w, int h) {
setColor(DARK);
fillRect(x - 1, y - 1, w + 2, h + 2);
setColor(LIGHT);
drawSoftRect(x, y, w, h);
}
// Format a small unread count into buf: "1".."99", then "99+". count >= 1.
// No stdio — DisplayDriver.h only pulls stdint/string.
// overflow: the count itself is honest (how many still-held entries are