mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
feat(ui): pill unread badges, unified DM/CH headers, trimmed home carousel
UI-polish trio from CODE_REVIEW (biggest "feels finished" gain per line): - Pill unread badges: new DisplayDriver::drawUnreadBadge()/unreadBadgeWidth() draw a filled capsule with the count knocked out (corners knocked back for a rounded look; inverts on a selected row). Replaces the bare right-aligned digits in MODE_SELECT, the contact/channel pickers and favourites tiles. No <stdio.h> in the header — fmtBadgeCount formats manually, clamps to 99+. - Unified DM/CH history headers: DM_HIST and CHANNEL_HIST drew their titles by hand (drawTextCentered + fillRect at lh+1), a different height/separator than every other screen. Both now route through drawCenteredHeader(). - Trimmed default home carousel: new NodePrefs::HP_DEFAULT (Clock, Tools, Shutdown, Favourites, Map; Messages + Settings always visible = 7 pages). applyDefaults() seeds it instead of HP_ALL. Recent/Radio/BT/Advert/GPS/Sensors are opt-in via Settings > Home Pages. Existing users keep their saved mask — factory default only; no migration, no schema bump. Both solo envs build green (OLED RAM 69.9%/Flash 62.8%; e-ink SUCCESS). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1543,7 +1543,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
|||||||
_prefs.ringtone2_bpm_idx = 2; // 120 bpm default
|
_prefs.ringtone2_bpm_idx = 2; // 120 bpm default
|
||||||
_prefs.notif_melody_ad = 0; // built-in advert sound by default
|
_prefs.notif_melody_ad = 0; // built-in advert sound by default
|
||||||
_prefs.advert_sound_scope = ADVERT_SOUND_SCOPE_ALL; // sound every advert by default
|
_prefs.advert_sound_scope = ADVERT_SOUND_SCOPE_ALL; // sound every advert by default
|
||||||
_prefs.home_pages_mask = NodePrefs::HP_ALL; // all pages visible (incl. Favourites + Map)
|
_prefs.home_pages_mask = NodePrefs::HP_DEFAULT; // curated everyday carousel; rest opt-in via Home Pages
|
||||||
_prefs.bot_enabled = 0;
|
_prefs.bot_enabled = 0;
|
||||||
_prefs.bot_channel_enabled = 0;
|
_prefs.bot_channel_enabled = 0;
|
||||||
_prefs.bot_channel_idx = 0;
|
_prefs.bot_channel_idx = 0;
|
||||||
|
|||||||
@@ -403,6 +403,13 @@ struct NodePrefs { // persisted to file
|
|||||||
static const uint16_t HP_FAVOURITES = 1 << HPB_FAVOURITES;
|
static const uint16_t HP_FAVOURITES = 1 << HPB_FAVOURITES;
|
||||||
static const uint16_t HP_MAP = 1 << HPB_MAP;
|
static const uint16_t HP_MAP = 1 << HPB_MAP;
|
||||||
static const uint16_t HP_ALL = 0x01FF | HP_FAVOURITES | HP_MAP;
|
static const uint16_t HP_ALL = 0x01FF | HP_FAVOURITES | HP_MAP;
|
||||||
|
// Factory-default carousel — the everyday pages only, so a fresh device isn't
|
||||||
|
// 13 pages to joystick through. Messages + Settings are always visible (no
|
||||||
|
// mask bit), so the mask covers: Clock, Tools, Shutdown, Favourites, Map.
|
||||||
|
// Recent / Radio / Bluetooth / Advert / GPS / Sensors are opt-in via
|
||||||
|
// Settings › Home Pages. Existing users keep their saved mask (loaded from
|
||||||
|
// /new_prefs); this only seeds brand-new / factory-reset devices.
|
||||||
|
static const uint16_t HP_DEFAULT = HP_CLOCK | HP_TOOLS | HP_SHUTDOWN | HP_FAVOURITES | HP_MAP;
|
||||||
|
|
||||||
// Label for home page by bit-index; returns "" for out-of-range.
|
// Label for home page by bit-index; returns "" for out-of-range.
|
||||||
// Array indices match HomePageBit values.
|
// Array indices match HomePageBit values.
|
||||||
|
|||||||
@@ -804,13 +804,8 @@ public:
|
|||||||
display.drawSelectionRow(0, y - 1, display.width(), item_h - 1, sel);
|
display.drawSelectionRow(0, y - 1, display.width(), item_h - 1, sel);
|
||||||
display.setCursor(2, y);
|
display.setCursor(2, y);
|
||||||
display.print(opts[i]);
|
display.print(opts[i]);
|
||||||
if (badges[i] > 0) {
|
if (badges[i] > 0)
|
||||||
char badge[5];
|
display.drawUnreadBadge(display.width() - 1, y, badges[i], sel);
|
||||||
snprintf(badge, sizeof(badge), "%d", badges[i]);
|
|
||||||
int bw = display.getTextWidth(badge) + 2;
|
|
||||||
display.setCursor(display.width() - bw, y);
|
|
||||||
display.print(badge);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
display.setColor(DisplayDriver::LIGHT);
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
if (_ctx_menu.active) _ctx_menu.render(display);
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
||||||
@@ -833,17 +828,10 @@ public:
|
|||||||
char filtered[sizeof(c.name)];
|
char filtered[sizeof(c.name)];
|
||||||
display.translateUTF8ToBlocks(filtered, c.name, sizeof(filtered));
|
display.translateUTF8ToBlocks(filtered, c.name, sizeof(filtered));
|
||||||
uint8_t dm_unread = _task->getDMUnread(c.id.pub_key);
|
uint8_t dm_unread = _task->getDMUnread(c.id.pub_key);
|
||||||
char badge[5] = "";
|
int bw = dm_unread > 0 ? display.unreadBadgeWidth(dm_unread) + 2 : 0;
|
||||||
int bw = 0;
|
display.drawTextEllipsized(2, y, display.width() - 2 - bw - reserve, filtered);
|
||||||
if (dm_unread > 0) {
|
if (dm_unread > 0)
|
||||||
snprintf(badge, sizeof(badge), "%d", (int)dm_unread);
|
display.drawUnreadBadge(display.width() - reserve, y, dm_unread, sel);
|
||||||
bw = display.getTextWidth(badge) + 2;
|
|
||||||
}
|
|
||||||
display.drawTextEllipsized(2, y, display.width() - 2 - bw - 1 - reserve, filtered);
|
|
||||||
if (dm_unread > 0) {
|
|
||||||
display.setCursor(display.width() - bw + 1 - reserve, y);
|
|
||||||
display.print(badge);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -863,17 +851,10 @@ public:
|
|||||||
ChannelDetails ch;
|
ChannelDetails ch;
|
||||||
if (the_mesh.getChannel(_channel_indices[list_idx], ch)) {
|
if (the_mesh.getChannel(_channel_indices[list_idx], ch)) {
|
||||||
uint8_t unread = _history.chUnread(_channel_indices[list_idx]);
|
uint8_t unread = _history.chUnread(_channel_indices[list_idx]);
|
||||||
char badge[5] = "";
|
int bw = unread > 0 ? display.unreadBadgeWidth(unread) + 2 : 0;
|
||||||
int bw = 0;
|
|
||||||
if (unread > 0) {
|
|
||||||
snprintf(badge, sizeof(badge), "%d", (int)unread);
|
|
||||||
bw = display.getTextWidth(badge) + 2;
|
|
||||||
}
|
|
||||||
display.drawTextEllipsized(2, y, display.width() - 4 - bw - reserve, ch.name);
|
display.drawTextEllipsized(2, y, display.width() - 4 - bw - reserve, ch.name);
|
||||||
if (unread > 0) {
|
if (unread > 0)
|
||||||
display.setCursor(display.width() - bw - reserve, y);
|
display.drawUnreadBadge(display.width() - reserve, y, unread, sel);
|
||||||
display.print(badge);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -913,10 +894,8 @@ public:
|
|||||||
int cby = display.height() - lh - 2;
|
int cby = display.height() - lh - 2;
|
||||||
|
|
||||||
char title[24];
|
char title[24];
|
||||||
display.setColor(DisplayDriver::LIGHT);
|
|
||||||
snprintf(title, sizeof(title), "%.23s", filtered_name);
|
snprintf(title, sizeof(title), "%.23s", filtered_name);
|
||||||
display.drawTextCentered(display.width()/2, 0, title);
|
display.drawCenteredHeader(title);
|
||||||
display.fillRect(0, lh + 1, display.width(), display.sepH());
|
|
||||||
|
|
||||||
int dm_count = _history.dmHistCountForContact(_sel_contact.id.pub_key);
|
int dm_count = _history.dmHistCountForContact(_sel_contact.id.pub_key);
|
||||||
uint32_t now_ts = rtc_clock.getCurrentTime();
|
uint32_t now_ts = rtc_clock.getCurrentTime();
|
||||||
@@ -1045,8 +1024,7 @@ public:
|
|||||||
the_mesh.getChannel(_sel_channel_idx, ch);
|
the_mesh.getChannel(_sel_channel_idx, ch);
|
||||||
char title[24];
|
char title[24];
|
||||||
snprintf(title, sizeof(title), "%.23s", ch.name);
|
snprintf(title, sizeof(title), "%.23s", ch.name);
|
||||||
display.drawTextCentered(display.width()/2, 0, title);
|
display.drawCenteredHeader(title);
|
||||||
display.fillRect(0, lh + 1, display.width(), display.sepH());
|
|
||||||
|
|
||||||
int ch_hist_count = _history.histCountForChannel(_sel_channel_idx);
|
int ch_hist_count = _history.histCountForChannel(_sel_channel_idx);
|
||||||
uint32_t now_ts = rtc_clock.getCurrentTime();
|
uint32_t now_ts = rtc_clock.getCurrentTime();
|
||||||
|
|||||||
@@ -1235,21 +1235,14 @@ public:
|
|||||||
|
|
||||||
// Reserve space for the unread badge so the name's ellipsis lands
|
// Reserve space for the unread badge so the name's ellipsis lands
|
||||||
// before it instead of underneath. Badge and name share one baseline.
|
// before it instead of underneath. Badge and name share one baseline.
|
||||||
char badge[5] = "";
|
|
||||||
int bw = 0;
|
|
||||||
uint8_t unread = _task->getDMUnread(ci.id.pub_key);
|
uint8_t unread = _task->getDMUnread(ci.id.pub_key);
|
||||||
if (unread > 0) {
|
int bw = unread > 0 ? display.unreadBadgeWidth(unread) + 3 : 0; // badge + 3 px gap
|
||||||
snprintf(badge, sizeof(badge), "%d", unread);
|
|
||||||
bw = display.getTextWidth(badge) + 3; // badge + 3 px gap
|
|
||||||
}
|
|
||||||
int name_y = cy + (cell_h - line_h) / 2;
|
int name_y = cy + (cell_h - line_h) / 2;
|
||||||
int name_max_w = cell_w - 4 - bw;
|
int name_max_w = cell_w - 4 - bw;
|
||||||
if (name_max_w < 6) name_max_w = 6;
|
if (name_max_w < 6) name_max_w = 6;
|
||||||
display.drawTextEllipsized(cx + 2, name_y, name_max_w, name);
|
display.drawTextEllipsized(cx + 2, name_y, name_max_w, name);
|
||||||
if (badge[0]) {
|
if (unread > 0)
|
||||||
display.setCursor(cx + cell_w - (bw - 3) - 2, name_y);
|
display.drawUnreadBadge(cx + cell_w - 2, name_y, unread, sel);
|
||||||
display.print(badge);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
int plus_y = cy + (cell_h - line_h) / 2;
|
int plus_y = cy + (cell_h - line_h) / 2;
|
||||||
display.drawTextCentered(cx + cell_w / 2, plus_y, "+");
|
display.drawTextCentered(cx + cell_w / 2, plus_y, "+");
|
||||||
|
|||||||
@@ -165,6 +165,45 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format a small unread count into buf: "1".."99", then "99+". count >= 1.
|
||||||
|
// No stdio — DisplayDriver.h only pulls stdint/string.
|
||||||
|
static void fmtBadgeCount(char* buf, int count) {
|
||||||
|
if (count > 99) { buf[0]='9'; buf[1]='9'; buf[2]='+'; buf[3]=0; }
|
||||||
|
else if (count >= 10) { buf[0]=(char)('0'+count/10); buf[1]=(char)('0'+count%10); buf[2]=0; }
|
||||||
|
else { buf[0]=(char)('0'+count); buf[1]=0; }
|
||||||
|
}
|
||||||
|
// Pixel width the pill from drawUnreadBadge(count) occupies — for reserving
|
||||||
|
// the name column before it. Mirrors the pill's horizontal padding.
|
||||||
|
int unreadBadgeWidth(int count) {
|
||||||
|
char buf[5]; fmtBadgeCount(buf, count);
|
||||||
|
return getTextWidth(buf) + (sepH() + 1) * 2;
|
||||||
|
}
|
||||||
|
// Right-aligned unread-count "pill": a filled capsule ending at right_x,
|
||||||
|
// aligned to a text row of height getLineHeight() at y, with the count
|
||||||
|
// knocked out. On a selected/inverted row pass sel=true so the pill inverts
|
||||||
|
// too (paper capsule + ink digits) and stays visible. The four corners are
|
||||||
|
// knocked back to the surrounding colour for a rounded-capsule look.
|
||||||
|
// Restores ink to LIGHT. Returns the pill width.
|
||||||
|
int drawUnreadBadge(int right_x, int y, int count, bool sel) {
|
||||||
|
char buf[5]; fmtBadgeCount(buf, count);
|
||||||
|
int pw = getTextWidth(buf) + (sepH() + 1) * 2;
|
||||||
|
int ph = getLineHeight();
|
||||||
|
int px = right_x - pw;
|
||||||
|
Color body = sel ? DARK : LIGHT;
|
||||||
|
Color ink = sel ? LIGHT : DARK;
|
||||||
|
setColor(body);
|
||||||
|
fillRect(px, y, pw, ph);
|
||||||
|
setColor(ink);
|
||||||
|
fillRect(px, y, 1, 1);
|
||||||
|
fillRect(px + pw - 1, y, 1, 1);
|
||||||
|
fillRect(px, y + ph - 1, 1, 1);
|
||||||
|
fillRect(px + pw - 1, y + ph - 1, 1, 1);
|
||||||
|
setCursor(px + sepH() + 1, y);
|
||||||
|
print(buf);
|
||||||
|
setColor(LIGHT);
|
||||||
|
return pw;
|
||||||
|
}
|
||||||
|
|
||||||
// Inverted title bar: light background, dark ellipsized label, then the
|
// Inverted title bar: light background, dark ellipsized label, then the
|
||||||
// standard separator line. The label is UTF-8 translated by
|
// standard separator line. The label is UTF-8 translated by
|
||||||
// drawTextEllipsized. Leaves ink colour LIGHT for following content.
|
// drawTextEllipsized. Leaves ink colour LIGHT for following content.
|
||||||
|
|||||||
Reference in New Issue
Block a user