mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in icons.h: a proportional thumb between fixed up/down triangle mini-icon caps. drawScrollIndicator() lives in a ~5px right-edge column and scales via miniIconScale (1x OLED, 2x landscape e-ink). - Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header separator) so caps stay visible on the LIGHT selection bar without a box. - Thumb is 3*s wide so it centres on the triangle column; caps are static end markers (always drawn) so neither vanishes at the extremes. - scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content, the selection bar and the message-history bubbles are all narrowed by it so nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the reserve in both the box-height pass and render so wrapped text can't spill. - Drop the redundant ">" selection marker (the highlight bar already shows selection) and shift rows to x=2, reclaiming left-edge space. - Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools, Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView, which scrolled with no indicator before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,7 @@ public:
|
||||
int val_x = display.valCol();
|
||||
_visible = display.listVisible(item_h);
|
||||
if (_visible < 1) _visible = 1;
|
||||
int reserve = scrollIndicatorReserve(display, ITEM_COUNT, _visible);
|
||||
|
||||
display.drawTextCentered(display.width() / 2, 0, "AUTO-REPLY BOT");
|
||||
// reply counter, right-aligned in the header
|
||||
@@ -91,7 +92,7 @@ public:
|
||||
int i = _scroll + vi;
|
||||
int y = start_y + vi * item_h;
|
||||
bool sel = (i == _sel);
|
||||
display.drawSelectionRow(0, y - 1, display.width(), item_h, sel);
|
||||
display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h, sel);
|
||||
display.setCursor(2, y);
|
||||
display.print(labels[i]);
|
||||
display.setCursor(val_x, y);
|
||||
@@ -104,7 +105,7 @@ public:
|
||||
} else {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(_prefs->bot_channel_idx, ch) && ch.name[0])
|
||||
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1, ch.name);
|
||||
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve,ch.name);
|
||||
else
|
||||
display.print("?");
|
||||
}
|
||||
@@ -113,10 +114,10 @@ public:
|
||||
const char* shown = !tr[0] ? "(none)"
|
||||
: (tr[0] == '*' && !tr[1]) ? "(any msg)" // wildcard / away mode
|
||||
: tr;
|
||||
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1, shown);
|
||||
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve,shown);
|
||||
} else if (i == 3 || i == 5) {
|
||||
const char* rp = (i == 3) ? _prefs->bot_reply_dm : _prefs->bot_reply_ch;
|
||||
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1, rp[0] ? rp : "(none)");
|
||||
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve,rp[0] ? rp : "(none)");
|
||||
} else if (i == 6) {
|
||||
display.print(_prefs->bot_commands_enabled ? "ON" : "OFF");
|
||||
} else { // i == 7 (quiet from) or i == 8 (quiet to)
|
||||
@@ -131,8 +132,7 @@ public:
|
||||
}
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
display.drawScrollArrows(start_y, start_y + (_visible - 1) * item_h,
|
||||
_scroll > 0, _scroll + _visible < ITEM_COUNT);
|
||||
drawScrollIndicator(display, start_y, _visible * item_h, ITEM_COUNT, _visible, _scroll);
|
||||
return 2000;
|
||||
}
|
||||
|
||||
|
||||
@@ -588,13 +588,14 @@ public:
|
||||
display.drawTextCentered(display.width() / 2, display.height() / 2 + display.lineStep() / 2, hint);
|
||||
}
|
||||
} else {
|
||||
int reserve = scrollIndicatorReserve(display, _count, _visible);
|
||||
for (int i = 0; i < _visible && (_scroll + i) < _count; i++) {
|
||||
int idx = _scroll + i;
|
||||
bool sel = (idx == _sel);
|
||||
int y = start_y + i * item_h;
|
||||
const Entry& e = _entries[idx];
|
||||
|
||||
display.drawSelectionRow(0, y - 1, display.width(), item_h - 1, sel);
|
||||
display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h - 1, sel);
|
||||
|
||||
char filt[32];
|
||||
display.translateUTF8ToBlocks(filt, e.name, sizeof(filt));
|
||||
@@ -620,12 +621,11 @@ public:
|
||||
if (e.dist_km >= 0.0f) geo::fmtDist(right, sizeof(right), e.dist_km, useImperial());
|
||||
else strncpy(right, "?GPS", sizeof(right));
|
||||
}
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, y);
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2 - reserve, y);
|
||||
display.print(right);
|
||||
}
|
||||
|
||||
display.drawScrollArrows(start_y, start_y + (_visible - 1) * item_h,
|
||||
_scroll > 0, _scroll + _visible < _count);
|
||||
drawScrollIndicator(display, start_y, _visible * item_h, _count, _visible, _scroll);
|
||||
}
|
||||
|
||||
if (renderActivePopup(display)) return 50;
|
||||
|
||||
@@ -834,9 +834,7 @@ public:
|
||||
int y = start_y + i * item_h;
|
||||
bool sel = (i == _mode_sel);
|
||||
display.drawSelectionRow(0, y - 1, display.width(), item_h - 1, sel);
|
||||
display.setCursor(0, y);
|
||||
display.print(sel ? ">" : " ");
|
||||
display.setCursor(cw + 2, y);
|
||||
display.setCursor(2, y);
|
||||
display.print(opts[i]);
|
||||
if (badges[i] > 0) {
|
||||
char badge[5];
|
||||
@@ -858,18 +856,17 @@ public:
|
||||
return 5000;
|
||||
}
|
||||
|
||||
int reserve = scrollIndicatorReserve(display, _num_contacts, _visible);
|
||||
for (int i = 0; i < _visible && (_contact_scroll+i) < _num_contacts; i++) {
|
||||
int list_idx = _contact_scroll + i;
|
||||
int mesh_idx = _sorted[list_idx];
|
||||
bool sel = (list_idx == _contact_sel);
|
||||
int y = start_y + i * item_h;
|
||||
|
||||
display.drawSelectionRow(0, y - 1, display.width(), item_h - 1, sel);
|
||||
display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h - 1, sel);
|
||||
|
||||
ContactInfo c;
|
||||
if (the_mesh.getContactByIdx(mesh_idx, c)) {
|
||||
display.setCursor(0, y);
|
||||
display.print(sel ? ">" : " ");
|
||||
char filtered[sizeof(c.name)];
|
||||
display.translateUTF8ToBlocks(filtered, c.name, sizeof(filtered));
|
||||
uint8_t dm_unread = _task->getDMUnread(c.id.pub_key);
|
||||
@@ -879,15 +876,14 @@ public:
|
||||
snprintf(badge, sizeof(badge), "%d", (int)dm_unread);
|
||||
bw = display.getTextWidth(badge) + 2;
|
||||
}
|
||||
display.drawTextEllipsized(cw + 2, y, display.width() - cw - 2 - bw - 1, filtered);
|
||||
display.drawTextEllipsized(2, y, display.width() - 2 - bw - 1 - reserve, filtered);
|
||||
if (dm_unread > 0) {
|
||||
display.setCursor(display.width() - bw + 1, y);
|
||||
display.setCursor(display.width() - bw + 1 - reserve, y);
|
||||
display.print(badge);
|
||||
}
|
||||
}
|
||||
}
|
||||
display.drawScrollArrows(start_y, start_y + (_visible-1)*item_h,
|
||||
_contact_scroll > 0, _contact_scroll + _visible < _num_contacts);
|
||||
drawScrollIndicator(display, start_y, _visible * item_h, _num_contacts, _visible, _contact_scroll);
|
||||
|
||||
// Context menu overlay
|
||||
if (_ctx_menu.active) _ctx_menu.render(display);
|
||||
@@ -901,14 +897,13 @@ public:
|
||||
return 5000;
|
||||
}
|
||||
|
||||
int reserve = scrollIndicatorReserve(display, _num_channels, _visible);
|
||||
for (int i = 0; i < _visible && (_channel_scroll+i) < _num_channels; i++) {
|
||||
int list_idx = _channel_scroll + i;
|
||||
bool sel = (list_idx == _channel_sel);
|
||||
int y = start_y + i * item_h;
|
||||
|
||||
display.drawSelectionRow(0, y - 1, display.width(), item_h - 1, sel);
|
||||
display.setCursor(0, y);
|
||||
display.print(sel ? ">" : " ");
|
||||
display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h - 1, sel);
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(_channel_indices[list_idx], ch)) {
|
||||
uint8_t unread = _ch_unread[_channel_indices[list_idx]];
|
||||
@@ -918,15 +913,14 @@ public:
|
||||
snprintf(badge, sizeof(badge), "%d", (int)unread);
|
||||
bw = display.getTextWidth(badge) + 2;
|
||||
}
|
||||
display.drawTextEllipsized(cw + 2, y, display.width() - cw - 4 - bw, ch.name);
|
||||
display.drawTextEllipsized(2, y, display.width() - 4 - bw - reserve, ch.name);
|
||||
if (unread > 0) {
|
||||
display.setCursor(display.width() - bw, y);
|
||||
display.setCursor(display.width() - bw - reserve, y);
|
||||
display.print(badge);
|
||||
}
|
||||
}
|
||||
}
|
||||
display.drawScrollArrows(start_y, start_y + (_visible-1)*item_h,
|
||||
_channel_scroll > 0, _channel_scroll + _visible < _num_channels);
|
||||
drawScrollIndicator(display, start_y, _visible * item_h, _num_channels, _visible, _channel_scroll);
|
||||
|
||||
// Context menu overlay
|
||||
if (_ctx_menu.active) _ctx_menu.render(display);
|
||||
@@ -974,6 +968,10 @@ public:
|
||||
bool portrait_expand = (display.height() > display.width());
|
||||
const int MAX_VIS_BOXES = 8;
|
||||
int box_ys[MAX_VIS_BOXES], box_hs[MAX_VIS_BOXES], n_vis = 0;
|
||||
// Scrollbar gutter, from last frame's visible count (this frame's isn't
|
||||
// known until the layout loop runs); keeps portrait wrap width and the box
|
||||
// width consistent so wrapped text never spills under the scrollbar.
|
||||
int reserve = scrollIndicatorReserve(display, dm_count, _hist_visible);
|
||||
{
|
||||
const int fixed_bh = 2 * lh + 1;
|
||||
int cur_y = hist_start_y;
|
||||
@@ -985,7 +983,7 @@ public:
|
||||
char trans[160];
|
||||
display.translateUTF8ToBlocks(trans, skipReplyPrefix(_dm_hist[rp].text), sizeof(trans));
|
||||
char wl[8][FS_CHARS_MAX];
|
||||
int nl = FullscreenMsgView::wrapLines(display, trans, display.width() - 6, wl, 8);
|
||||
int nl = FullscreenMsgView::wrapLines(display, trans, display.width() - 6 - reserve, wl, 8);
|
||||
bh = (1 + (nl > 0 ? nl : 1)) * lh + 1;
|
||||
}
|
||||
}
|
||||
@@ -995,7 +993,6 @@ public:
|
||||
}
|
||||
}
|
||||
_hist_visible = n_vis;
|
||||
|
||||
for (int i = 0; i < n_vis && (_dm_hist_scroll + i) < dm_count; i++) {
|
||||
int item = _dm_hist_scroll + i;
|
||||
bool sel = (item == _dm_hist_sel);
|
||||
@@ -1013,26 +1010,26 @@ public:
|
||||
|
||||
if (sel) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, y, display.width(), bh);
|
||||
display.fillRect(0, y, display.width() - reserve, bh);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, y, display.width(), bh);
|
||||
display.fillRect(1, y + 1, display.width() - 2, lh);
|
||||
display.drawRect(0, y, display.width() - reserve, bh);
|
||||
display.fillRect(1, y + 1, display.width() - 2 - reserve, lh);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
}
|
||||
display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender);
|
||||
display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w - reserve, sender);
|
||||
if (e.outgoing) { // delivery marker after "Me"
|
||||
int gx = 3 + display.getTextWidth(sender) + 3;
|
||||
drawAckGlyph(display, gx, y + 1, dmEffectiveStatus(e), e.attempt + 1);
|
||||
}
|
||||
if (age[0]) { display.setCursor(display.width() - age_w, y + 1); display.print(age); }
|
||||
if (age[0]) { display.setCursor(display.width() - age_w - reserve, y + 1); display.print(age); }
|
||||
if (!sel) display.setColor(DisplayDriver::LIGHT);
|
||||
if (portrait_expand) {
|
||||
char trans[160];
|
||||
display.translateUTF8ToBlocks(trans, skipReplyPrefix(e.text), sizeof(trans));
|
||||
char wl[8][FS_CHARS_MAX];
|
||||
int nl = FullscreenMsgView::wrapLines(display, trans, display.width() - 6, wl, 8);
|
||||
int nl = FullscreenMsgView::wrapLines(display, trans, display.width() - 6 - reserve, wl, 8);
|
||||
for (int li = 0; li < nl; li++) { display.setCursor(3, y + (li + 1) * lh + 1); display.print(wl[li]); }
|
||||
} else {
|
||||
display.drawTextEllipsized(3, y + lh + 1, display.width() - cw - 2, skipReplyPrefix(e.text));
|
||||
@@ -1046,8 +1043,8 @@ public:
|
||||
|
||||
{
|
||||
int arrow_y = (n_vis > 0) ? box_ys[n_vis - 1] + box_hs[n_vis - 1] - lh : hist_start_y;
|
||||
display.drawScrollArrows(hist_start_y + 1, arrow_y,
|
||||
_dm_hist_scroll > 0, _dm_hist_scroll + _hist_visible < dm_count);
|
||||
drawScrollIndicator(display, hist_start_y + 1, arrow_y + lh - (hist_start_y + 1),
|
||||
dm_count, _hist_visible, _dm_hist_scroll);
|
||||
}
|
||||
|
||||
bool compose_sel = (_dm_hist_sel == -1);
|
||||
@@ -1113,6 +1110,8 @@ public:
|
||||
bool portrait_expand = (display.height() > display.width());
|
||||
const int MAX_VIS_BOXES = 8;
|
||||
int box_ys[MAX_VIS_BOXES], box_hs[MAX_VIS_BOXES], n_vis = 0;
|
||||
// Scrollbar gutter, from last frame's visible count (see DM history above).
|
||||
int reserve = scrollIndicatorReserve(display, ch_hist_count, _hist_visible);
|
||||
{
|
||||
const int fixed_bh = 2 * lh + 1;
|
||||
int cur_y = hist_start_y;
|
||||
@@ -1127,7 +1126,7 @@ public:
|
||||
char trans[160];
|
||||
display.translateUTF8ToBlocks(trans, skipReplyPrefix(rbody), sizeof(trans));
|
||||
char wl[8][FS_CHARS_MAX];
|
||||
int nl = FullscreenMsgView::wrapLines(display, trans, display.width() - 6, wl, 8);
|
||||
int nl = FullscreenMsgView::wrapLines(display, trans, display.width() - 6 - reserve, wl, 8);
|
||||
bh = (1 + (nl > 0 ? nl : 1)) * lh + 1;
|
||||
}
|
||||
}
|
||||
@@ -1166,15 +1165,15 @@ public:
|
||||
|
||||
if (sel) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, y, display.width(), bh);
|
||||
display.fillRect(0, y, display.width() - reserve, bh);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, y, display.width(), bh);
|
||||
display.fillRect(1, y + 1, display.width() - 2, lh);
|
||||
display.drawRect(0, y, display.width() - reserve, bh);
|
||||
display.fillRect(1, y + 1, display.width() - 2 - reserve, lh);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
}
|
||||
display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender);
|
||||
display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w - reserve, sender);
|
||||
// Channels have no recipient ACK — only show ✓ once a repeater echo
|
||||
// confirms the send was relayed into the mesh; otherwise no marker
|
||||
// (absence is normal, not a failure).
|
||||
@@ -1182,13 +1181,13 @@ public:
|
||||
int gx = 3 + display.getTextWidth(sender) + 3;
|
||||
drawAckGlyph(display, gx, y + 1, ACK_OK);
|
||||
}
|
||||
if (age[0]) { display.setCursor(display.width() - age_w, y + 1); display.print(age); }
|
||||
if (age[0]) { display.setCursor(display.width() - age_w - reserve, y + 1); display.print(age); }
|
||||
if (!sel) display.setColor(DisplayDriver::LIGHT);
|
||||
if (portrait_expand) {
|
||||
char trans[160];
|
||||
display.translateUTF8ToBlocks(trans, skipReplyPrefix(msg_part), sizeof(trans));
|
||||
char wl[8][FS_CHARS_MAX];
|
||||
int nl = FullscreenMsgView::wrapLines(display, trans, display.width() - 6, wl, 8);
|
||||
int nl = FullscreenMsgView::wrapLines(display, trans, display.width() - 6 - reserve, wl, 8);
|
||||
for (int li = 0; li < nl; li++) { display.setCursor(3, y + (li + 1) * lh + 1); display.print(wl[li]); }
|
||||
} else {
|
||||
display.drawTextEllipsized(3, y + lh + 1, display.width() - cw - 2, skipReplyPrefix(msg_part));
|
||||
@@ -1203,8 +1202,8 @@ public:
|
||||
// scroll hints
|
||||
{
|
||||
int arrow_y = (n_vis > 0) ? box_ys[n_vis - 1] + box_hs[n_vis - 1] - lh : hist_start_y;
|
||||
display.drawScrollArrows(hist_start_y + 1, arrow_y,
|
||||
_hist_scroll > 0, _hist_scroll + _hist_visible < ch_hist_count);
|
||||
drawScrollIndicator(display, hist_start_y + 1, arrow_y + lh - (hist_start_y + 1),
|
||||
ch_hist_count, _hist_visible, _hist_scroll);
|
||||
}
|
||||
|
||||
// small compose button (bottom-left, always bordered, inverted when selected)
|
||||
@@ -1249,27 +1248,25 @@ public:
|
||||
display.fillRect(0, display.headerH() - 1, display.width(), display.sepH());
|
||||
|
||||
int total_msg_items = 1 + _active_msg_count;
|
||||
int reserve = scrollIndicatorReserve(display, total_msg_items, _visible);
|
||||
for (int i = 0; i < _visible && (_msg_scroll+i) < total_msg_items; i++) {
|
||||
int idx = _msg_scroll + i;
|
||||
bool sel = (idx == _msg_sel);
|
||||
int y = start_y + i * item_h;
|
||||
|
||||
display.drawSelectionRow(0, y - 1, display.width(), item_h - 1, sel);
|
||||
display.setCursor(0, y);
|
||||
display.print(sel ? ">" : " ");
|
||||
display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h - 1, sel);
|
||||
|
||||
if (idx == 0) {
|
||||
display.setCursor(cw + 2, y);
|
||||
display.setCursor(2, y);
|
||||
display.print("Custom message...");
|
||||
} else {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
int slot = _active_msgs[idx - 1];
|
||||
const char* tmpl = p ? p->custom_msgs[slot] : "";
|
||||
display.drawTextEllipsized(cw + 2, y, display.width() - cw - 4, tmpl);
|
||||
display.drawTextEllipsized(2, y, display.width() - 4 - reserve, tmpl);
|
||||
}
|
||||
}
|
||||
display.drawScrollArrows(start_y, start_y + (_visible-1)*item_h,
|
||||
_msg_scroll > 0, _msg_scroll + _visible < total_msg_items);
|
||||
drawScrollIndicator(display, start_y, _visible * item_h, total_msg_items, _visible, _msg_scroll);
|
||||
}
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@@ -74,6 +74,7 @@ class SettingsScreen : public UIScreen {
|
||||
int _selected;
|
||||
int _scroll;
|
||||
int _visible; // items fitting on screen; updated each render, used by handleInput
|
||||
int _reserve = 0; // right-edge px reserved for the scrollbar (0 when list fits)
|
||||
bool _dirty;
|
||||
uint8_t _collapsed = 0x7F; // bit N set = section N collapsed (Display=0..Messages=6)
|
||||
static const int MAX_VIS = 60;
|
||||
@@ -112,7 +113,7 @@ class SettingsScreen : public UIScreen {
|
||||
|
||||
void renderBar(DisplayDriver& display, int x, int y, int value, int max_val) {
|
||||
const int gap = 2;
|
||||
const int avail = display.width() - x;
|
||||
const int avail = display.width() - x - _reserve;
|
||||
const int raw = (avail - (max_val - 1) * gap) / max_val;
|
||||
const int cap = display.getLineHeight() - 2;
|
||||
const int box_h = raw < cap ? (raw < 2 ? 2 : raw) : cap;
|
||||
@@ -402,10 +403,8 @@ class SettingsScreen : public UIScreen {
|
||||
bool collapsed = (_collapsed >> si) & 1;
|
||||
bool sel = (item == _selected);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawSelectionRow(0, y - 1, display.width(), display.lineStep(), sel);
|
||||
display.setCursor(0, y);
|
||||
display.print(sel ? ">" : " ");
|
||||
display.setCursor(display.getCharWidth() + 2, y);
|
||||
display.drawSelectionRow(0, y - 1, display.width() - _reserve, display.lineStep(), sel);
|
||||
display.setCursor(2, y);
|
||||
display.print(collapsed ? "+" : "-");
|
||||
display.print(" ");
|
||||
display.print(sectionName(item));
|
||||
@@ -413,11 +412,9 @@ class SettingsScreen : public UIScreen {
|
||||
}
|
||||
|
||||
bool sel = (item == _selected);
|
||||
display.drawSelectionRow(0, y - 1, display.width(), display.lineStep(), sel);
|
||||
display.drawSelectionRow(0, y - 1, display.width() - _reserve, display.lineStep(), sel);
|
||||
|
||||
display.setCursor(0, y);
|
||||
display.print(sel ? ">" : " ");
|
||||
display.setCursor(display.getCharWidth() + 2, y);
|
||||
display.setCursor(2, y);
|
||||
|
||||
#if FEAT_BRIGHTNESS_SETTING
|
||||
if (item == BRIGHTNESS) {
|
||||
@@ -471,7 +468,7 @@ class SettingsScreen : public UIScreen {
|
||||
display.print(pb);
|
||||
}
|
||||
display.print(homePageLabel(item));
|
||||
display.setCursor(display.width() - 6 * display.getCharWidth(), y);
|
||||
display.setCursor(display.width() - 6 * display.getCharWidth() - _reserve, y);
|
||||
if (!homePageToggleable(item))
|
||||
display.print("always");
|
||||
else
|
||||
@@ -584,7 +581,7 @@ class SettingsScreen : public UIScreen {
|
||||
display.print(label);
|
||||
const char* tmpl = (p && p->custom_msgs[slot][0]) ? p->custom_msgs[slot] : "(empty)";
|
||||
int xm = 8 + display.getCharWidth() * 4;
|
||||
display.drawTextEllipsized(xm, y, display.width() - xm, tmpl);
|
||||
display.drawTextEllipsized(xm, y, display.width() - xm - _reserve, tmpl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,6 +614,7 @@ public:
|
||||
int item_h = display.lineStep();
|
||||
int start_y = display.listStart();
|
||||
_visible = display.listVisible(item_h);
|
||||
_reserve = scrollIndicatorReserve(display, _vis_count, _visible);
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(display.width() / 2, 0, "SETTINGS");
|
||||
@@ -626,8 +624,7 @@ public:
|
||||
renderItem(display, _vis[_scroll + i], start_y + i * item_h);
|
||||
}
|
||||
|
||||
display.drawScrollArrows(start_y, start_y + (_visible - 1) * item_h,
|
||||
_scroll > 0, _scroll + _visible < _vis_count);
|
||||
drawScrollIndicator(display, start_y, _visible * item_h, _vis_count, _visible, _scroll);
|
||||
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ public:
|
||||
|
||||
int item_h = display.lineStep();
|
||||
int start_y = display.listStart();
|
||||
int cw = display.getCharWidth();
|
||||
int vis = display.listVisible(item_h);
|
||||
if (vis < 1) vis = 1;
|
||||
|
||||
@@ -29,18 +28,16 @@ public:
|
||||
if (_sel < _scroll) _scroll = _sel;
|
||||
if (_sel >= _scroll + vis) _scroll = _sel - vis + 1;
|
||||
|
||||
int reserve = scrollIndicatorReserve(display, ITEM_COUNT, vis);
|
||||
for (int i = 0; i < vis && (_scroll + i) < ITEM_COUNT; i++) {
|
||||
int idx = _scroll + i;
|
||||
int y = start_y + i * item_h;
|
||||
bool sel = (idx == _sel);
|
||||
display.drawSelectionRow(0, y - 1, display.width(), item_h, sel);
|
||||
display.setCursor(0, y);
|
||||
display.print(sel ? ">" : " ");
|
||||
display.setCursor(cw + 2, y);
|
||||
display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h, sel);
|
||||
display.setCursor(2, y);
|
||||
display.print(ITEMS[idx]);
|
||||
}
|
||||
display.drawScrollArrows(start_y, start_y + (vis - 1) * item_h,
|
||||
_scroll > 0, _scroll + vis < ITEM_COUNT);
|
||||
drawScrollIndicator(display, start_y, vis * item_h, ITEM_COUNT, vis, _scroll);
|
||||
return 500;
|
||||
}
|
||||
|
||||
|
||||
@@ -412,9 +412,7 @@ private:
|
||||
display.print(buf);
|
||||
}
|
||||
|
||||
display.drawScrollArrows(y0, y0 + (visible - 1) * step,
|
||||
_summary_scroll > 0,
|
||||
_summary_scroll + visible < SUMMARY_ITEM_COUNT);
|
||||
drawScrollIndicator(display, y0, visible * step, SUMMARY_ITEM_COUNT, visible, _summary_scroll);
|
||||
}
|
||||
|
||||
void renderList(DisplayDriver& display) {
|
||||
@@ -466,8 +464,7 @@ private:
|
||||
display.print(row);
|
||||
}
|
||||
|
||||
display.drawScrollArrows(top, top + (visible - 1) * step,
|
||||
_list_scroll > 0, _list_scroll + visible < total);
|
||||
drawScrollIndicator(display, top, visible * step, total, visible, _list_scroll);
|
||||
}
|
||||
|
||||
// Shared map projection: geographic (1e-6 deg) → screen pixels. The scale is
|
||||
|
||||
@@ -131,18 +131,16 @@ class WaypointsView {
|
||||
display.fillRect(0, display.headerH() - 1, display.width(), display.sepH());
|
||||
const int top = display.listStart();
|
||||
const int step = display.lineStep();
|
||||
const int cw = display.getCharWidth();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int y = top + i * step;
|
||||
bool sel = (i == _add_sel);
|
||||
display.drawSelectionRow(0, y - 1, display.width(), step - 1, sel);
|
||||
display.setCursor(0, y); display.print(sel ? ">" : " ");
|
||||
char row[28];
|
||||
if (i == 0) snprintf(row, sizeof(row), "Lat: %c %s", _add_lat_neg ? 'S' : 'N', _add_lat[0] ? _add_lat : "--");
|
||||
else if (i == 1) snprintf(row, sizeof(row), "Lon: %c %s", _add_lon_neg ? 'W' : 'E', _add_lon[0] ? _add_lon : "--");
|
||||
else if (i == 2) snprintf(row, sizeof(row), "Label: %s", _add_label[0] ? _add_label : "(auto)");
|
||||
else snprintf(row, sizeof(row), "[Save]");
|
||||
display.setCursor(cw + 2, y); display.print(row);
|
||||
display.setCursor(2, y); display.print(row);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
}
|
||||
@@ -159,7 +157,6 @@ class WaypointsView {
|
||||
int total = n + 1; // final row = "+ Add by coords"
|
||||
const int top = display.listStart();
|
||||
const int step = display.lineStep();
|
||||
const int cw = display.getCharWidth();
|
||||
int vis = display.listVisible();
|
||||
if (vis < 1) vis = 1;
|
||||
if (_sel < _scroll) _scroll = _sel;
|
||||
@@ -167,15 +164,15 @@ class WaypointsView {
|
||||
|
||||
int32_t mylat, mylon; bool have = ownPos(mylat, mylon);
|
||||
|
||||
int reserve = scrollIndicatorReserve(display, total, vis);
|
||||
for (int i = 0; i < vis && (_scroll + i) < total; i++) {
|
||||
int row = _scroll + i;
|
||||
int y = top + i * step;
|
||||
bool sel = (row == _sel);
|
||||
display.drawSelectionRow(0, y - 1, display.width(), step - 1, sel);
|
||||
display.setCursor(0, y); display.print(sel ? ">" : " ");
|
||||
display.drawSelectionRow(0, y - 1, display.width() - reserve, step - 1, sel);
|
||||
|
||||
if (row == n) { // the synthetic "Add" row
|
||||
display.setCursor(cw + 2, y); display.print("+ Add by coords");
|
||||
display.setCursor(2, y); display.print("+ Add by coords");
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
continue;
|
||||
}
|
||||
@@ -189,10 +186,11 @@ class WaypointsView {
|
||||
}
|
||||
char nm[24];
|
||||
display.translateUTF8ToBlocks(nm, label, sizeof(nm));
|
||||
display.drawTextEllipsized(cw + 2, y, display.width() - cw - 2 - bw, nm);
|
||||
if (dist[0]) { display.setCursor(display.width() - bw + 1, y); display.print(dist); }
|
||||
display.drawTextEllipsized(2, y, display.width() - 2 - bw - reserve, nm);
|
||||
if (dist[0]) { display.setCursor(display.width() - bw + 1 - reserve, y); display.print(dist); }
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
drawScrollIndicator(display, top, vis * step, total, vis, _scroll);
|
||||
}
|
||||
|
||||
void renderWpNav(DisplayDriver& display) {
|
||||
|
||||
@@ -65,6 +65,37 @@ inline void miniIconDraw(DisplayDriver& d, int x, int top_y, const MiniIcon& ic)
|
||||
miniIconDraw(d, x, top_y, ic.rows, ic.w, ic.h);
|
||||
}
|
||||
|
||||
// Draw a mini-icon at an exact top-left (no vertical centring). Used where the
|
||||
// caller controls placement, e.g. flush to the top/bottom of a scroll track.
|
||||
inline void miniIconDrawTop(DisplayDriver& d, int x, int y, const MiniIcon& ic) {
|
||||
const int s = miniIconScale(d);
|
||||
for (int r = 0; r < ic.h; r++)
|
||||
for (int c = 0; c < ic.w; c++)
|
||||
if (ic.rows[r] & (1 << c)) d.fillRect(x + c * s, y + r * s, s, s);
|
||||
}
|
||||
|
||||
// Like miniIconDrawTop, but first lays down a 1px DARK halo that hugs the icon's
|
||||
// shape (each pixel dilated by 1px), then the icon in LIGHT. Invisible on a dark
|
||||
// row; on the LIGHT selection bar it outlines just the glyph — no boxed-in
|
||||
// rectangle around it. Restores ink to LIGHT. clip_top bounds the halo so it
|
||||
// can't bleed above a given y (e.g. onto a header separator just above the icon).
|
||||
inline void miniIconDrawHalo(DisplayDriver& d, int x, int y, const MiniIcon& ic,
|
||||
int clip_top = -100000) {
|
||||
const int s = miniIconScale(d);
|
||||
d.setColor(DisplayDriver::DARK);
|
||||
for (int r = 0; r < ic.h; r++)
|
||||
for (int c = 0; c < ic.w; c++)
|
||||
if (ic.rows[r] & (1 << c)) {
|
||||
int hy = y + r * s - 1, hh = s + 2;
|
||||
if (hy < clip_top) { hh -= clip_top - hy; hy = clip_top; }
|
||||
if (hh > 0) d.fillRect(x + c * s - 1, hy, s + 2, hh);
|
||||
}
|
||||
d.setColor(DisplayDriver::LIGHT);
|
||||
for (int r = 0; r < ic.h; r++)
|
||||
for (int c = 0; c < ic.w; c++)
|
||||
if (ic.rows[r] & (1 << c)) d.fillRect(x + c * s, y + r * s, s, s);
|
||||
}
|
||||
|
||||
// Horizontal row of `count` square dots (scaled, vertically centred). Used by
|
||||
// the "awaiting ACK" marker, where the dot count = number of send attempts.
|
||||
inline void miniIconDotRow(DisplayDriver& d, int x, int top_y, int count) {
|
||||
@@ -117,6 +148,79 @@ MINI_ICON(ICON_SPACE_R, 8,
|
||||
packRow(".......#"),
|
||||
packRow("########"));
|
||||
|
||||
// Scroll-indicator caps — small up/down triangles (authored on the 1× grid).
|
||||
MINI_ICON(ICON_SCROLL_UP, 5, // ▲
|
||||
packRow("..#.."),
|
||||
packRow(".###."),
|
||||
packRow("#####"));
|
||||
MINI_ICON(ICON_SCROLL_DOWN, 5, // ▼
|
||||
packRow("#####"),
|
||||
packRow(".###."),
|
||||
packRow("..#.."));
|
||||
|
||||
// Width of the right-edge column drawScrollIndicator occupies, or 0 when the
|
||||
// list fits and no indicator is drawn. Subtract from a row's content width so
|
||||
// text never runs under the scrollbar. Mirrors the column math below (5*scale
|
||||
// triangle + 1px gap).
|
||||
inline int scrollIndicatorReserve(DisplayDriver& d, int total, int visible) {
|
||||
return (total > visible) ? 5 * miniIconScale(d) + 1 : 0;
|
||||
}
|
||||
|
||||
// Right-edge scroll indicator: a proportional track + thumb topped/tailed with
|
||||
// up/down triangle mini-icon caps. Drop-in replacement for
|
||||
// DisplayDriver::drawScrollArrows that also shows how much of the list is on
|
||||
// screen and where you are within it. Everything lives in a ~5px column at the
|
||||
// right edge and scales with the font (mini-icon scale).
|
||||
// top_y : y of the first visible row (top of the viewport)
|
||||
// track_h : pixel height of the viewport (e.g. visible_rows * row_pitch)
|
||||
// total : total number of items
|
||||
// visible : items shown at once
|
||||
// first : index of the first visible item (scroll offset)
|
||||
inline void drawScrollIndicator(DisplayDriver& d, int top_y, int track_h,
|
||||
int total, int visible, int first) {
|
||||
if (total <= visible || track_h <= 0) return; // whole list fits — no indicator
|
||||
if (first < 0) first = 0;
|
||||
if (first > total - visible) first = total - visible;
|
||||
|
||||
const int s = miniIconScale(d);
|
||||
const int col = 5 * s; // triangle / column width
|
||||
const int th = 3 * s; // triangle height
|
||||
const int x = d.width() - col; // indicator column origin
|
||||
|
||||
// Caps are static end-markers, always drawn flush at the very top / bottom of
|
||||
// the track; the thumb travels in the fixed band between them. (They mark the
|
||||
// track ends, not "more above/below", so they never vanish at the extremes.)
|
||||
const int cap = th + 2; // triangle height + 2px gap
|
||||
const int band_t = top_y + cap;
|
||||
const int band_b = top_y + track_h - cap;
|
||||
int band = band_b - band_t;
|
||||
if (band < s) band = s;
|
||||
|
||||
const int bar_w = 3 * s; // odd width → centres exactly in the 5*s column
|
||||
const int bar_x = x + (col - bar_w) / 2;
|
||||
|
||||
int thumb_h = (int)((long)band * visible / total);
|
||||
if (thumb_h < th) thumb_h = th;
|
||||
if (thumb_h > band) thumb_h = band;
|
||||
const int span = total - visible; // > 0 here (total > visible)
|
||||
const int thumb_y = band_t + (int)((long)(band - thumb_h) * first / span);
|
||||
|
||||
// Each marker is drawn with a 1px DARK halo: invisible on a normal (dark) row,
|
||||
// but on the LIGHT selection bar it carves out contrast so the LIGHT marker
|
||||
// stays visible. Avoids having to know which row is currently selected.
|
||||
// Thumb: a rectangle, so a plain box halo matches its shape.
|
||||
d.setColor(DisplayDriver::DARK);
|
||||
d.fillRect(bar_x - 1, thumb_y - 1, bar_w + 2, thumb_h + 2);
|
||||
d.setColor(DisplayDriver::LIGHT);
|
||||
d.fillRect(bar_x, thumb_y, bar_w, thumb_h);
|
||||
|
||||
// Arrows: shape-hugging halo so they aren't boxed in on the selection bar.
|
||||
// The top arrow clips its halo at top_y so it can't bite into the header
|
||||
// separator sitting one pixel above the list area.
|
||||
miniIconDrawHalo(d, x, top_y, ICON_SCROLL_UP, top_y);
|
||||
miniIconDrawHalo(d, x, top_y + track_h - th, ICON_SCROLL_DOWN);
|
||||
}
|
||||
|
||||
// ── Big ASCII-art icons (skeleton, not yet used) ─────────────────────────────
|
||||
// Same authoring idea as the mini-icons but for full page glyphs up to 32 px
|
||||
// wide: one uint32_t per row. The existing XBM bitmaps below (logo/bluetooth/…)
|
||||
|
||||
Reference in New Issue
Block a user