mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-02 02:06:11 +00:00
feat(ui): discoverable context-menu hint — header glyph, highlight, corner dropdown
Hold-Enter context menus were invisible. Add a menu affordance to every screen that has one: - A small ≡ glyph in the header's top-right, via new DisplayDriver:: drawContextMenuHint() and an optional menu_hint arg on drawCenteredHeader() / drawInvertedHeader() (reserves the corner so the title never runs under it). - The glyph highlights (corner cell filled, bars knocked out) while the menu is open, tying the hint to the popup it spawned — driven by a menu_open flag the screens pass from their live popup state. - The context menu now drops out of that corner: PopupMenu gains an opt-in top-right anchor (begin(..., anchor_top_right)); it right-aligns under the header instead of centring, so the menu reads as emerging from the ≡. Default stays centred, so non-context popups (Tools/Settings/etc.) are untouched. Enabled on Nodes (list/scan/detail) and Messages (mode select, contact/room and channel pickers, DM and channel history). Both solo envs build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -415,7 +415,7 @@ class NearbyScreen : public UIScreen {
|
|||||||
void startDeleteConfirm() {
|
void startDeleteConfirm() {
|
||||||
const Entry* e = selected();
|
const Entry* e = selected();
|
||||||
if (!e || !e->has_key || !entryIsContact(e)) return;
|
if (!e || !e->has_key || !entryIsContact(e)) return;
|
||||||
_confirm.begin("Delete contact?", 2);
|
_confirm.begin("Delete contact?", 2, true);
|
||||||
_confirm.addItem("Delete");
|
_confirm.addItem("Delete");
|
||||||
_confirm.addItem("Cancel");
|
_confirm.addItem("Cancel");
|
||||||
_confirm.setSelected(1);
|
_confirm.setSelected(1);
|
||||||
@@ -451,7 +451,7 @@ class NearbyScreen : public UIScreen {
|
|||||||
|
|
||||||
void rebuildPingMenu() {
|
void rebuildPingMenu() {
|
||||||
int keep = _ping_menu.selectedIndex(); // preserve selection across a rebuild
|
int keep = _ping_menu.selectedIndex(); // preserve selection across a rebuild
|
||||||
_ping_menu.begin("Ping", 4);
|
_ping_menu.begin("Ping", 4, true);
|
||||||
_ping_menu.addItem("Send");
|
_ping_menu.addItem("Send");
|
||||||
if (_ping_time_str[0]) _ping_menu.addItem(_ping_time_str);
|
if (_ping_time_str[0]) _ping_menu.addItem(_ping_time_str);
|
||||||
if (_ping_snr_out_str[0]) _ping_menu.addItem(_ping_snr_out_str);
|
if (_ping_snr_out_str[0]) _ping_menu.addItem(_ping_snr_out_str);
|
||||||
@@ -543,7 +543,7 @@ class NearbyScreen : public UIScreen {
|
|||||||
|
|
||||||
buildSortLabel();
|
buildSortLabel();
|
||||||
_menu_action_count = 0;
|
_menu_action_count = 0;
|
||||||
_menu.begin("Options", 10);
|
_menu.begin("Options", 10, true);
|
||||||
auto add = [&](const char* label, Action a) {
|
auto add = [&](const char* label, Action a) {
|
||||||
_menu.addItem(label);
|
_menu.addItem(label);
|
||||||
_menu_actions[_menu_action_count++] = a;
|
_menu_actions[_menu_action_count++] = a;
|
||||||
@@ -611,7 +611,7 @@ class NearbyScreen : public UIScreen {
|
|||||||
void renderStoredDetail(DisplayDriver& display) {
|
void renderStoredDetail(DisplayDriver& display) {
|
||||||
const Entry& e = _entries[_sel];
|
const Entry& e = _entries[_sel];
|
||||||
const int hdr = display.listStart(); // content top (gap below the header separator)
|
const int hdr = display.listStart(); // content top (gap below the header separator)
|
||||||
display.drawInvertedHeader(e.name);
|
display.drawInvertedHeader(e.name, true, ctxMenuOpen());
|
||||||
|
|
||||||
int step = display.lineStep();
|
int step = display.lineStep();
|
||||||
if (step * 5 > display.height() - hdr) step = (display.height() - hdr) / 5;
|
if (step * 5 > display.height() - hdr) step = (display.height() - hdr) / 5;
|
||||||
@@ -648,7 +648,7 @@ class NearbyScreen : public UIScreen {
|
|||||||
char label[32];
|
char label[32];
|
||||||
if (e.name[0]) { strncpy(label, e.name, 31); label[31] = '\0'; }
|
if (e.name[0]) { strncpy(label, e.name, 31); label[31] = '\0'; }
|
||||||
else { snprintf(label, sizeof(label), "[%s]", typeName(e.type)); }
|
else { snprintf(label, sizeof(label), "[%s]", typeName(e.type)); }
|
||||||
display.drawInvertedHeader(label);
|
display.drawInvertedHeader(label, true, ctxMenuOpen());
|
||||||
|
|
||||||
char b64[48];
|
char b64[48];
|
||||||
pubKeyToBase64(e.pub_key, b64, sizeof(b64));
|
pubKeyToBase64(e.pub_key, b64, sizeof(b64));
|
||||||
@@ -679,6 +679,10 @@ class NearbyScreen : public UIScreen {
|
|||||||
display.print(e.is_known ? "Status: known" : "Status: new");
|
display.print(e.is_known ? "Status: known" : "Status: new");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Any Hold-Enter popup on screen → highlight the ≡ hint so it reads as the
|
||||||
|
// source of the open menu.
|
||||||
|
bool ctxMenuOpen() const { return _menu.active || _confirm.active || _ping_menu.active; }
|
||||||
|
|
||||||
// Draw whichever popup is active over the current view. Returns true if one was.
|
// Draw whichever popup is active over the current view. Returns true if one was.
|
||||||
bool renderActivePopup(DisplayDriver& display) {
|
bool renderActivePopup(DisplayDriver& display) {
|
||||||
updatePingMenuState();
|
updatePingMenuState();
|
||||||
@@ -719,6 +723,7 @@ class NearbyScreen : public UIScreen {
|
|||||||
|
|
||||||
int lx = ax - gap; // next left tab's right edge
|
int lx = ax - gap; // next left tab's right edge
|
||||||
int rx = ax + w[_filter] + gap; // next right tab's left edge
|
int rx = ax + w[_filter] + gap; // next right tab's left edge
|
||||||
|
int rx_limit = display.width() - display.menuHintWidth(); // leave room for the ≡ hint
|
||||||
bool lfit = true, rfit = true;
|
bool lfit = true, rfit = true;
|
||||||
for (int k = 1; k <= F_COUNT / 2 && (lfit || rfit); k++) {
|
for (int k = 1; k <= F_COUNT / 2 && (lfit || rfit); k++) {
|
||||||
int li = (_filter - k + F_COUNT) % F_COUNT;
|
int li = (_filter - k + F_COUNT) % F_COUNT;
|
||||||
@@ -731,12 +736,13 @@ class NearbyScreen : public UIScreen {
|
|||||||
// Skip the right side when it lands on the same tab as the left (the single
|
// 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.
|
// opposite tab on an even count) so it isn't drawn twice.
|
||||||
if (rfit && ri != li) {
|
if (rfit && ri != li) {
|
||||||
if (rx + w[ri] <= display.width()) { drawTab(display, rx, w[ri], FILTER_LABELS[ri], false); rx += w[ri] + gap; }
|
if (rx + w[ri] <= rx_limit) { drawTab(display, rx, w[ri], FILTER_LABELS[ri], false); rx += w[ri] + gap; }
|
||||||
else rfit = false;
|
else rfit = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
display.setColor(DisplayDriver::LIGHT);
|
display.setColor(DisplayDriver::LIGHT);
|
||||||
display.fillRect(0, display.headerH() - display.sepH(), display.width(), display.sepH());
|
display.fillRect(0, display.headerH() - display.sepH(), display.width(), display.sepH());
|
||||||
|
display.drawContextMenuHint(DisplayDriver::LIGHT, ctxMenuOpen()); // Nodes list has a Hold-Enter menu
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -825,7 +831,7 @@ public:
|
|||||||
} else if (_scanning) snprintf(title, sizeof(title), "SCANNING (%d)", _count);
|
} else if (_scanning) snprintf(title, sizeof(title), "SCANNING (%d)", _count);
|
||||||
else if (_count == 0) snprintf(title, sizeof(title), "SCAN: none");
|
else if (_count == 0) snprintf(title, sizeof(title), "SCAN: none");
|
||||||
else snprintf(title, sizeof(title), "SCAN (%d)", _count);
|
else snprintf(title, sizeof(title), "SCAN (%d)", _count);
|
||||||
display.drawCenteredHeader(title);
|
display.drawCenteredHeader(title, true, ctxMenuOpen());
|
||||||
} else {
|
} else {
|
||||||
// Stored list: the filter is a visible tab strip (LEFT/RIGHT switches tabs).
|
// Stored list: the filter is a visible tab strip (LEFT/RIGHT switches tabs).
|
||||||
drawFilterTabs(display);
|
drawFilterTabs(display);
|
||||||
|
|||||||
@@ -17,14 +17,16 @@ struct PopupMenu {
|
|||||||
int _cap; // actual visible cap, updated each render()
|
int _cap; // actual visible cap, updated each render()
|
||||||
bool active;
|
bool active;
|
||||||
const char* _title;
|
const char* _title;
|
||||||
|
bool anchor_tr = false; // opt-in: drop from the top-right ≡ hint instead of centring
|
||||||
|
|
||||||
enum Result { NONE, SELECTED, CANCELLED };
|
enum Result { NONE, SELECTED, CANCELLED };
|
||||||
|
|
||||||
PopupMenu() : _count(0), _sel(0), _scroll(0), _visible(3), _cap(3), active(false), _title(nullptr) {}
|
PopupMenu() : _count(0), _sel(0), _scroll(0), _visible(3), _cap(3), active(false), _title(nullptr) {}
|
||||||
|
|
||||||
void begin(const char* title, int visible = 3) {
|
void begin(const char* title, int visible = 3, bool anchor_top_right = false) {
|
||||||
_count = 0; _sel = 0; _scroll = 0;
|
_count = 0; _sel = 0; _scroll = 0;
|
||||||
_visible = visible; _cap = visible; active = true; _title = title;
|
_visible = visible; _cap = visible; active = true; _title = title;
|
||||||
|
anchor_tr = anchor_top_right;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addItem(const char* item) {
|
void addItem(const char* item) {
|
||||||
@@ -86,8 +88,17 @@ struct PopupMenu {
|
|||||||
int bh = title_h + vis * item_h + 1; // +1 keeps the last row off the bottom border
|
int bh = title_h + vis * item_h + 1; // +1 keeps the last row off the bottom border
|
||||||
|
|
||||||
// Centre on screen (clamped to the margins on small displays).
|
// Centre on screen (clamped to the margins on small displays).
|
||||||
int bx = (display.width() - bw) / 2;
|
int bx, by;
|
||||||
int by = (display.height() - bh) / 2;
|
if (anchor_tr) {
|
||||||
|
// Drop out of the top-right ≡ hint: right-aligned under the header, so the
|
||||||
|
// menu reads as emerging from the glyph that spawned it.
|
||||||
|
bx = display.width() - bw - margin;
|
||||||
|
by = display.headerH();
|
||||||
|
if (by + bh > display.height() - margin) by = display.height() - margin - bh;
|
||||||
|
} else {
|
||||||
|
bx = (display.width() - bw) / 2;
|
||||||
|
by = (display.height() - bh) / 2;
|
||||||
|
}
|
||||||
if (bx < margin) bx = margin;
|
if (bx < margin) bx = margin;
|
||||||
if (by < margin) by = margin;
|
if (by < margin) by = margin;
|
||||||
|
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ class QuickMsgScreen : public UIScreen {
|
|||||||
int n = (reply_allowed ? 1 : 0) + (has_loc ? 2 : 0);
|
int n = (reply_allowed ? 1 : 0) + (has_loc ? 2 : 0);
|
||||||
if (n == 0) return;
|
if (n == 0) return;
|
||||||
_fs_act_n = 0;
|
_fs_act_n = 0;
|
||||||
_ctx_menu.begin("Options", n);
|
_ctx_menu.begin("Options", n, true);
|
||||||
if (reply_allowed) { _ctx_menu.addItem("Reply"); _fs_act[_fs_act_n++] = FS_REPLY; }
|
if (reply_allowed) { _ctx_menu.addItem("Reply"); _fs_act[_fs_act_n++] = FS_REPLY; }
|
||||||
if (has_loc) { _ctx_menu.addItem("Navigate"); _fs_act[_fs_act_n++] = FS_NAV;
|
if (has_loc) { _ctx_menu.addItem("Navigate"); _fs_act[_fs_act_n++] = FS_NAV;
|
||||||
_ctx_menu.addItem("Save waypoint"); _fs_act[_fs_act_n++] = FS_SAVE; }
|
_ctx_menu.addItem("Save waypoint"); _fs_act[_fs_act_n++] = FS_SAVE; }
|
||||||
@@ -791,7 +791,7 @@ public:
|
|||||||
int cw = display.getCharWidth();
|
int cw = display.getCharWidth();
|
||||||
|
|
||||||
if (_phase == MODE_SELECT) {
|
if (_phase == MODE_SELECT) {
|
||||||
display.drawCenteredHeader("MESSAGE");
|
display.drawCenteredHeader("MESSAGE", true, _ctx_menu.active);
|
||||||
const char* opts[] = { "Direct message", "Channels", "Room Servers" };
|
const char* opts[] = { "Direct message", "Channels", "Room Servers" };
|
||||||
int badges[3] = {
|
int badges[3] = {
|
||||||
getDMUnreadTotal(),
|
getDMUnreadTotal(),
|
||||||
@@ -811,7 +811,7 @@ public:
|
|||||||
if (_ctx_menu.active) _ctx_menu.render(display);
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
||||||
|
|
||||||
} else if (_phase == CONTACT_PICK) {
|
} else if (_phase == CONTACT_PICK) {
|
||||||
display.drawCenteredHeader(_room_mode ? "SELECT ROOM" : "SELECT CONTACT");
|
display.drawCenteredHeader(_room_mode ? "SELECT ROOM" : "SELECT CONTACT", true, _ctx_menu.active);
|
||||||
|
|
||||||
if (_num_contacts == 0) {
|
if (_num_contacts == 0) {
|
||||||
display.drawTextCentered(display.width()/2, display.height()/2, _room_mode ? "No room servers" : "No favourites");
|
display.drawTextCentered(display.width()/2, display.height()/2, _room_mode ? "No room servers" : "No favourites");
|
||||||
@@ -839,7 +839,7 @@ public:
|
|||||||
if (_ctx_menu.active) _ctx_menu.render(display);
|
if (_ctx_menu.active) _ctx_menu.render(display);
|
||||||
|
|
||||||
} else if (_phase == CHANNEL_PICK) {
|
} else if (_phase == CHANNEL_PICK) {
|
||||||
display.drawCenteredHeader("SELECT CHANNEL");
|
display.drawCenteredHeader("SELECT CHANNEL", true, _ctx_menu.active);
|
||||||
|
|
||||||
if (_num_channels == 0) {
|
if (_num_channels == 0) {
|
||||||
display.drawTextCentered(display.width()/2, display.height()/2, "No channels");
|
display.drawTextCentered(display.width()/2, display.height()/2, "No channels");
|
||||||
@@ -895,7 +895,7 @@ public:
|
|||||||
|
|
||||||
char title[24];
|
char title[24];
|
||||||
snprintf(title, sizeof(title), "%.23s", filtered_name);
|
snprintf(title, sizeof(title), "%.23s", filtered_name);
|
||||||
display.drawCenteredHeader(title);
|
display.drawCenteredHeader(title, true, _ctx_menu.active);
|
||||||
|
|
||||||
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();
|
||||||
@@ -1024,7 +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.drawCenteredHeader(title);
|
display.drawCenteredHeader(title, true, _ctx_menu.active);
|
||||||
|
|
||||||
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();
|
||||||
@@ -1200,7 +1200,7 @@ public:
|
|||||||
if (c == KEY_CONTEXT_MENU) {
|
if (c == KEY_CONTEXT_MENU) {
|
||||||
// PopupMenu stores the title pointer verbatim — use static strings.
|
// PopupMenu stores the title pointer verbatim — use static strings.
|
||||||
static const char* MODE_TITLES[] = { "DM options", "Channel options", "Room options" };
|
static const char* MODE_TITLES[] = { "DM options", "Channel options", "Room options" };
|
||||||
_ctx_menu.begin(MODE_TITLES[_mode_sel < 3 ? _mode_sel : 0], 1);
|
_ctx_menu.begin(MODE_TITLES[_mode_sel < 3 ? _mode_sel : 0], 1, true);
|
||||||
_ctx_menu.addItem("Mark all read");
|
_ctx_menu.addItem("Mark all read");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1314,7 +1314,7 @@ public:
|
|||||||
snprintf(_pin_slot_labels[s], sizeof(_pin_slot_labels[s]), "Slot %d: %s", s + 1, nm);
|
snprintf(_pin_slot_labels[s], sizeof(_pin_slot_labels[s]), "Slot %d: %s", s + 1, nm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ctx_menu.begin("Pick slot", 3);
|
_ctx_menu.begin("Pick slot", 3, true);
|
||||||
for (int s = 0; s < NodePrefs::FAVOURITES_COUNT; s++) _ctx_menu.addItem(_pin_slot_labels[s]);
|
for (int s = 0; s < NodePrefs::FAVOURITES_COUNT; s++) _ctx_menu.addItem(_pin_slot_labels[s]);
|
||||||
_pin_picker_active = true;
|
_pin_picker_active = true;
|
||||||
}
|
}
|
||||||
@@ -1355,7 +1355,7 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (c == KEY_CONTEXT_MENU && _num_contacts > 0 && _room_mode) {
|
if (c == KEY_CONTEXT_MENU && _num_contacts > 0 && _room_mode) {
|
||||||
_ctx_menu.begin("Room options", 1);
|
_ctx_menu.begin("Room options", 1, true);
|
||||||
_ctx_menu.addItem("Login...");
|
_ctx_menu.addItem("Login...");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1371,7 +1371,7 @@ public:
|
|||||||
int pinned_slot = _task->findFavouriteSlot(ci.id.pub_key);
|
int pinned_slot = _task->findFavouriteSlot(ci.id.pub_key);
|
||||||
if (pinned_slot >= 0) snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Unpin (slot %d)", pinned_slot + 1);
|
if (pinned_slot >= 0) snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Unpin (slot %d)", pinned_slot + 1);
|
||||||
else snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Pin to dial");
|
else snprintf(_ctx_pin_item, sizeof(_ctx_pin_item), "Pin to dial");
|
||||||
_ctx_menu.begin("Contact options", 3);
|
_ctx_menu.begin("Contact options", 3, true);
|
||||||
_ctx_menu.addItem("Mark as read");
|
_ctx_menu.addItem("Mark as read");
|
||||||
_ctx_menu.addItem(_ctx_notif_item);
|
_ctx_menu.addItem(_ctx_notif_item);
|
||||||
_ctx_menu.addItem(_ctx_melody_item);
|
_ctx_menu.addItem(_ctx_melody_item);
|
||||||
@@ -1471,7 +1471,7 @@ public:
|
|||||||
{ NodePrefs* p2 = _task->getNodePrefs();
|
{ NodePrefs* p2 = _task->getNodePrefs();
|
||||||
bool is_fav = p2 && (p2->ch_fav_bitmask & (1ULL << ch_idx));
|
bool is_fav = p2 && (p2->ch_fav_bitmask & (1ULL << ch_idx));
|
||||||
snprintf(_ctx_ch_fav_item, sizeof(_ctx_ch_fav_item), is_fav ? "Fav: yes" : "Fav: no"); }
|
snprintf(_ctx_ch_fav_item, sizeof(_ctx_ch_fav_item), is_fav ? "Fav: yes" : "Fav: no"); }
|
||||||
_ctx_menu.begin("Channel options", 4);
|
_ctx_menu.begin("Channel options", 4, true);
|
||||||
_ctx_menu.addItem("Mark all read");
|
_ctx_menu.addItem("Mark all read");
|
||||||
_ctx_menu.addItem(_ctx_notif_item);
|
_ctx_menu.addItem(_ctx_notif_item);
|
||||||
_ctx_menu.addItem(_ctx_melody_item);
|
_ctx_menu.addItem(_ctx_melody_item);
|
||||||
|
|||||||
@@ -207,23 +207,52 @@ public:
|
|||||||
// 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.
|
||||||
void drawInvertedHeader(const char* label) {
|
// Pixel width the ≡ context-menu hint reserves at the header's right edge.
|
||||||
|
int menuHintWidth() const { return getCharWidth() + 3; }
|
||||||
|
|
||||||
|
// Small ≡ glyph at the header's top-right, signalling the screen has a
|
||||||
|
// Hold-Enter context menu. Three stacked bars, drawn in colour c (LIGHT on a
|
||||||
|
// plain header, DARK on an inverted bar). When active (the menu is open) the
|
||||||
|
// corner cell is highlighted and the bars knocked out, tying the glyph to the
|
||||||
|
// popup it spawned. Call after the header body.
|
||||||
|
void drawContextMenuHint(Color c = LIGHT, bool active = false) {
|
||||||
|
int gw = getCharWidth() + 1;
|
||||||
|
int gx = width() - gw - 1;
|
||||||
|
int th = sepH();
|
||||||
|
int gy = (getLineHeight() - (th * 3 + 4)) / 2;
|
||||||
|
if (gy < 0) gy = 0;
|
||||||
|
Color bars = c;
|
||||||
|
if (active) {
|
||||||
|
setColor(c);
|
||||||
|
fillRect(gx - 1, 0, gw + 2, headerH() - sepH());
|
||||||
|
bars = (c == LIGHT) ? DARK : LIGHT;
|
||||||
|
}
|
||||||
|
setColor(bars);
|
||||||
|
for (int i = 0; i < 3; i++) fillRect(gx, gy + i * (th + 2), gw, th);
|
||||||
|
setColor(LIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawInvertedHeader(const char* label, bool menu_hint = false, bool menu_open = false) {
|
||||||
int hdr = headerH();
|
int hdr = headerH();
|
||||||
setColor(LIGHT);
|
setColor(LIGHT);
|
||||||
fillRect(0, 0, width(), hdr - 1);
|
fillRect(0, 0, width(), hdr - 1);
|
||||||
|
int reserve = menu_hint ? menuHintWidth() : 0;
|
||||||
setColor(DARK);
|
setColor(DARK);
|
||||||
drawTextEllipsized(2, 1, width() - 4, (label && label[0]) ? label : "");
|
drawTextEllipsized(2, 1, width() - 4 - reserve, (label && label[0]) ? label : "");
|
||||||
|
if (menu_hint) drawContextMenuHint(DARK, menu_open);
|
||||||
setColor(LIGHT);
|
setColor(LIGHT);
|
||||||
fillRect(0, hdr - 1, width(), sepH());
|
fillRect(0, hdr - 1, width(), sepH());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Centred screen title + bottom separator line — the standard top bar for
|
// Centred screen title + bottom separator line — the standard top bar for
|
||||||
// full-screen list/detail views. Leaves ink LIGHT; callers can draw extra
|
// full-screen list/detail views. Leaves ink LIGHT; callers can draw extra
|
||||||
// header content (counters, etc.) afterwards.
|
// header content (counters, etc.) afterwards. menu_hint adds the ≡ glyph;
|
||||||
void drawCenteredHeader(const char* title) {
|
// menu_open highlights it while the context menu is on screen.
|
||||||
|
void drawCenteredHeader(const char* title, bool menu_hint = false, bool menu_open = false) {
|
||||||
setColor(LIGHT);
|
setColor(LIGHT);
|
||||||
drawTextCentered(width() / 2, 0, title);
|
drawTextCentered(width() / 2, 0, title);
|
||||||
fillRect(0, headerH() - sepH(), width(), sepH());
|
fillRect(0, headerH() - sepH(), width(), sepH());
|
||||||
|
if (menu_hint) drawContextMenuHint(LIGHT, menu_open);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance a UTF-8 pointer by one codepoint, returning the decoded value.
|
// Advance a UTF-8 pointer by one codepoint, returning the decoded value.
|
||||||
|
|||||||
Reference in New Issue
Block a user