mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-09-14 15:16:40 +00:00
feat(ui): marquee-scroll selected long names/labels instead of static ellipsis
Selecting a row whose ellipsized text overflows now animates a "swing" marquee: holds at the start, scrolls to reveal the full tail, holds there, then scrolls back and repeats. Unselected/non-overflowing text is unchanged (still a static "..."). E-ink gets slower, coarser steps (fewer, cheaper partial refreshes) than OLED; unchanged frames are already skipped by the display's CRC diff, so idle holds are free. Wired into every screen with a selectable row: home favourites, DM/ channel lists and message bodies, Settings, popup menus, Bot, Admin, Nearby, Waypoints, Locator, Live Share, and the alarm screen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -514,6 +514,7 @@ public:
|
||||
}
|
||||
tabbar::draw(display, TAB_LABELS, ATAB_COUNT, _tab);
|
||||
int n = ROWS_PER_TAB[_tab];
|
||||
int mq_delay = 0;
|
||||
drawList(display, n, _row_sel, _row_scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
drawRowSelection(display, y, sel, reserve);
|
||||
const AdminField& f = fieldAt(_tab, i);
|
||||
@@ -523,7 +524,8 @@ public:
|
||||
// the value being edited -- only rows without an inline value get the
|
||||
// full row width.
|
||||
int label_max = show_val ? display.valCol() - 4 : display.width() - 4 - reserve;
|
||||
display.drawTextEllipsized(2, y, label_max, f.label);
|
||||
int r = display.drawTextEllipsized(2, y, label_max, f.label, sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
if (show_val) {
|
||||
if (f.kind == FK_RADIO_FREQ) {
|
||||
// valCol() reserves exactly the 8-char width this editor draws (4
|
||||
@@ -540,7 +542,8 @@ public:
|
||||
}
|
||||
});
|
||||
if (_confirm.active) { _confirm.render(display); return 50; }
|
||||
return _value_editing ? 50 : 2000;
|
||||
if (_value_editing) return 50;
|
||||
return (mq_delay > 0 && mq_delay < 2000) ? mq_delay : 2000;
|
||||
}
|
||||
|
||||
// REPLY
|
||||
|
||||
@@ -149,6 +149,7 @@ public:
|
||||
}
|
||||
|
||||
int n = ROWS_PER_TAB[_tab];
|
||||
int mq_delay = 0;
|
||||
drawList(display, n, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
Row r = tabRow(_tab, i);
|
||||
drawRowSelection(display, y, sel, reserve);
|
||||
@@ -173,9 +174,10 @@ public:
|
||||
ChannelDetails ch;
|
||||
if (_num_channels == 0)
|
||||
display.print("(none)");
|
||||
else if (the_mesh.getChannel(_prefs->bot_channel_idx, ch) && ch.name[0])
|
||||
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve, ch.name);
|
||||
else
|
||||
else if (the_mesh.getChannel(_prefs->bot_channel_idx, ch) && ch.name[0]) {
|
||||
int mqr = display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve, ch.name, sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
} else
|
||||
display.print("?");
|
||||
break;
|
||||
}
|
||||
@@ -184,9 +186,10 @@ public:
|
||||
display.print("(none)");
|
||||
} else {
|
||||
ContactInfo* c = the_mesh.lookupContactByPubKey(_prefs->bot_room_prefix, NodePrefs::FAVOURITE_PREFIX_LEN);
|
||||
if (c && c->name[0])
|
||||
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve, c->name);
|
||||
else
|
||||
if (c && c->name[0]) {
|
||||
int mqr = display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve, c->name, sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
} else
|
||||
display.print("?");
|
||||
}
|
||||
break;
|
||||
@@ -200,7 +203,8 @@ 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 - reserve, shown);
|
||||
int mqr = display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve, shown, sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
break;
|
||||
}
|
||||
case REPLY_DM:
|
||||
@@ -209,7 +213,8 @@ public:
|
||||
const char* rp = (r.kind == REPLY_DM) ? _prefs->bot_reply_dm
|
||||
: (r.kind == REPLY_CH) ? _prefs->bot_reply_ch
|
||||
: _prefs->bot_reply_room;
|
||||
display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve, rp[0] ? rp : "(none)");
|
||||
int mqr = display.drawTextEllipsized(val_x, y, display.width() - val_x - 1 - reserve, rp[0] ? rp : "(none)", sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
break;
|
||||
}
|
||||
case COMMANDS_DM:
|
||||
|
||||
@@ -188,6 +188,7 @@ public:
|
||||
|
||||
if (_mode == ADD_HASHTAG) {
|
||||
display.drawCenteredHeader("ADD CHANNEL");
|
||||
int mq_delay = 0;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
int y = top + i * step;
|
||||
bool sel = (i == _sel);
|
||||
@@ -195,13 +196,15 @@ public:
|
||||
char row[40];
|
||||
if (i == 0) snprintf(row, sizeof(row), "Topic: %s", _topic[0] ? _topic : "(none)");
|
||||
else snprintf(row, sizeof(row), "[Save]");
|
||||
display.drawTextEllipsized(2, y, display.width() - 4, row);
|
||||
int r = display.drawTextEllipsized(2, y, display.width() - 4, row, sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
return 1000;
|
||||
return (mq_delay > 0 && mq_delay < 1000) ? mq_delay : 1000;
|
||||
}
|
||||
|
||||
display.drawCenteredHeader(_mode == ADD ? "ADD CHANNEL" : "EDIT CHANNEL");
|
||||
int mq_delay = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int y = top + i * step;
|
||||
bool sel = (i == _sel);
|
||||
@@ -213,10 +216,11 @@ public:
|
||||
else snprintf(row, sizeof(row), "[Save]");
|
||||
// Ellipsize rather than print() directly -- a long name/secret must not
|
||||
// wrap onto the next row's line (print() wraps by default).
|
||||
display.drawTextEllipsized(2, y, display.width() - 4, row);
|
||||
int r = display.drawTextEllipsized(2, y, display.width() - 4, row, sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
return 1000;
|
||||
return (mq_delay > 0 && mq_delay < 1000) ? mq_delay : 1000;
|
||||
}
|
||||
|
||||
bool handleInput(char c) {
|
||||
|
||||
@@ -166,19 +166,22 @@ class ClockToolsScreen : public UIScreen {
|
||||
const char* rows[3] = { "Time", "Repeat", "Armed" };
|
||||
if (_sel > 2) _sel = 2;
|
||||
const int valx = d.width() / 2 + 6;
|
||||
int mq_delay = 0;
|
||||
drawList(d, 3, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
drawRowSelection(d, y, sel, reserve);
|
||||
d.setCursor(4, y); d.print(rows[i]);
|
||||
if (i == 0) {
|
||||
drawAlarmTime(d, y, valx, sel && _alarm_editing);
|
||||
} else if (i == 1) {
|
||||
d.drawTextEllipsized(valx, y, d.width() - valx - reserve,
|
||||
NodePrefs::alarmRepeatLabel(NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask)));
|
||||
int mqr = d.drawTextEllipsized(valx, y, d.width() - valx - reserve,
|
||||
NodePrefs::alarmRepeatLabel(NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask)), sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
} else {
|
||||
d.drawTextEllipsized(valx, y, d.width() - valx - reserve, _prefs->alarm_on ? "ON" : "OFF");
|
||||
}
|
||||
});
|
||||
return _alarm_editing ? 50 : 60000;
|
||||
if (_alarm_editing) return 50;
|
||||
return (mq_delay > 0 && mq_delay < 60000) ? mq_delay : 60000;
|
||||
}
|
||||
|
||||
int renderTimer(DisplayDriver& d) {
|
||||
|
||||
@@ -90,6 +90,7 @@ public:
|
||||
display.drawCenteredHeader("LIVE SHARE");
|
||||
|
||||
const int valx = display.width() / 2 + 6;
|
||||
int mq_delay = 0;
|
||||
drawList(display, ROW_COUNT, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
Row r = rows(i);
|
||||
drawRowSelection(display, y, sel, reserve);
|
||||
@@ -97,9 +98,12 @@ public:
|
||||
display.print(r.label);
|
||||
char val[24];
|
||||
valueLabel(r.kind, val, sizeof(val));
|
||||
if (val[0]) display.drawTextEllipsized(valx, y, display.width() - valx - reserve, val);
|
||||
if (val[0]) {
|
||||
int mqr = display.drawTextEllipsized(valx, y, display.width() - valx - reserve, val, sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
}
|
||||
});
|
||||
return 500;
|
||||
return (mq_delay > 0 && mq_delay < 500) ? mq_delay : 500;
|
||||
}
|
||||
|
||||
void moveSel(int dir) { _sel = (_sel + dir + ROW_COUNT) % ROW_COUNT; }
|
||||
|
||||
@@ -108,12 +108,13 @@ public:
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
if (_picking) { renderPicker(display); return 400; }
|
||||
if (_picking) { int mq = renderPicker(display); return (mq > 0 && mq < 400) ? mq : 400; }
|
||||
display.drawCenteredHeader("LOCATOR");
|
||||
|
||||
const int rc = visibleRows();
|
||||
if (_sel >= rc) _sel = rc - 1; // beeper row may have just been hidden
|
||||
const int valx = display.width() / 2 + 6;
|
||||
int mq_delay = 0;
|
||||
drawList(display, rc, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
Row r = rows(i);
|
||||
drawRowSelection(display, y, sel, reserve);
|
||||
@@ -121,9 +122,12 @@ public:
|
||||
display.print(r.label);
|
||||
char val[24];
|
||||
valueLabel(r.kind, val, sizeof(val));
|
||||
if (val[0]) display.drawTextEllipsized(valx, y, display.width() - valx - reserve, val);
|
||||
if (val[0]) {
|
||||
int mqr = display.drawTextEllipsized(valx, y, display.width() - valx - reserve, val, sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
}
|
||||
});
|
||||
return 500;
|
||||
return (mq_delay > 0 && mq_delay < 500) ? mq_delay : 500;
|
||||
}
|
||||
|
||||
void moveSel(int dir) { int rc = visibleRows(); _sel = (_sel + dir + rc) % rc; }
|
||||
@@ -261,9 +265,12 @@ public:
|
||||
_picking = true;
|
||||
}
|
||||
|
||||
void renderPicker(DisplayDriver& display) {
|
||||
// Returns 0, or the ms until the selected row should next redraw to keep a
|
||||
// marquee animation going (see DisplayDriver::drawTextEllipsized).
|
||||
int renderPicker(DisplayDriver& display) {
|
||||
display.drawCenteredHeader("PICK TARGET");
|
||||
uint32_t now = rtc_clock.getCurrentTime();
|
||||
int mq_delay = 0;
|
||||
drawList(display, _target_n, _pick_sel, _pick_scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
drawRowSelection(display, y, sel, reserve);
|
||||
const Target& t = _targets[i];
|
||||
@@ -279,8 +286,10 @@ public:
|
||||
} else {
|
||||
snprintf(row, sizeof(row), "@%s", t.name); // favourite, no position known yet
|
||||
}
|
||||
display.drawTextEllipsized(2, y, display.width() - 2 - reserve, row);
|
||||
int mqr = display.drawTextEllipsized(2, y, display.width() - 2 - reserve, row, sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
});
|
||||
return mq_delay;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
|
||||
@@ -955,6 +955,7 @@ public:
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
int mq_delay = 0; // >0 while a selected row's text is marquee-scrolling
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
@@ -1007,7 +1008,8 @@ public:
|
||||
display.translateUTF8ToBlocks(filtered, c.name, sizeof(filtered));
|
||||
uint8_t dm_unread = _task->getDMUnread(c.id.pub_key);
|
||||
int bw = dm_unread > 0 ? display.unreadBadgeWidth(dm_unread) + 2 : 0;
|
||||
display.drawTextEllipsized(2, y, display.width() - 2 - bw - reserve, filtered);
|
||||
int r = display.drawTextEllipsized(2, y, display.width() - 2 - bw - reserve, filtered, sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
if (dm_unread > 0)
|
||||
display.drawUnreadBadge(display.width() - reserve, y, dm_unread, sel);
|
||||
}
|
||||
@@ -1040,7 +1042,8 @@ public:
|
||||
if (the_mesh.getChannel(_channel_indices[list_idx], ch)) {
|
||||
uint8_t unread = _history.chUnread(_channel_indices[list_idx]);
|
||||
int bw = unread > 0 ? display.unreadBadgeWidth(unread) + 2 : 0;
|
||||
display.drawTextEllipsized(2, y, display.width() - 4 - bw - reserve, ch.name);
|
||||
int r = display.drawTextEllipsized(2, y, display.width() - 4 - bw - reserve, ch.name, sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
if (unread > 0)
|
||||
display.drawUnreadBadge(display.width() - reserve, y, unread, sel);
|
||||
}
|
||||
@@ -1179,6 +1182,9 @@ public:
|
||||
BubbleBox box = computeBubbleBox(full_avail, e.outgoing, header_w, body_w);
|
||||
|
||||
drawHistRowFrame(display, box.x, box.w, y, bh, lh, sel);
|
||||
// Only the body marquees, not the sender too: both share the single
|
||||
// marquee slot on DisplayDriver, and if two texts in the same row both
|
||||
// qualified they'd keep resetting each other's animation every frame.
|
||||
display.drawTextEllipsized(box.x + 3, y + 1, box.w - 6 - age_w, sender);
|
||||
if (e.outgoing) { // delivery marker after "Me"
|
||||
int gx = box.x + 3 + display.getTextWidth(sender) + 3;
|
||||
@@ -1189,7 +1195,8 @@ public:
|
||||
if (portrait_expand) {
|
||||
for (int li = 0; li < nl; li++) { display.setCursor(box.x + 3, y + (li + 1) * lh + 1); display.print(s_wrap_lines[li]); }
|
||||
} else {
|
||||
display.drawTextEllipsized(box.x + 3, y + lh + 1, box.w - 6, body);
|
||||
int r_body = display.drawTextEllipsized(box.x + 3, y + lh + 1, box.w - 6, body, sel);
|
||||
if (sel && r_body > 0) mq_delay = r_body;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1213,7 +1220,7 @@ public:
|
||||
|
||||
drawComposeButton(display, cby, lh, _dm_hist_sel == -1);
|
||||
if (_ctx_menu.active) _ctx_menu.render(display);
|
||||
return dm_count > 0 ? 500 : 2000;
|
||||
{ int ret = dm_count > 0 ? 500 : 2000; return (mq_delay > 0 && mq_delay < ret) ? mq_delay : ret; }
|
||||
|
||||
} else if (_phase == CHANNEL_HIST) {
|
||||
if (_fs.active && _hist_sel >= 0) {
|
||||
@@ -1354,6 +1361,7 @@ public:
|
||||
BubbleBox box = computeBubbleBox(full_avail, outgoing, header_w, body_w);
|
||||
|
||||
drawHistRowFrame(display, box.x, box.w, y, bh, lh, sel);
|
||||
// Only the body marquees, not the sender — see the DM history block above.
|
||||
display.drawTextEllipsized(box.x + 3, y + 1, box.w - 6 - age_w, sender);
|
||||
if (show_ack) {
|
||||
int gx = box.x + 3 + display.getTextWidth(sender) + 3;
|
||||
@@ -1364,7 +1372,8 @@ public:
|
||||
if (portrait_expand) {
|
||||
for (int li = 0; li < nl; li++) { display.setCursor(box.x + 3, y + (li + 1) * lh + 1); display.print(s_wrap_lines[li]); }
|
||||
} else {
|
||||
display.drawTextEllipsized(box.x + 3, y + lh + 1, box.w - 6, body);
|
||||
int r_body = display.drawTextEllipsized(box.x + 3, y + lh + 1, box.w - 6, body, sel);
|
||||
if (sel && r_body > 0) mq_delay = r_body;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1421,11 +1430,12 @@ public:
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
int slot = _active_msgs[idx - 1];
|
||||
const char* tmpl = p ? p->custom_msgs[slot] : "";
|
||||
display.drawTextEllipsized(2, y, display.width() - 4 - reserve, tmpl);
|
||||
int r = display.drawTextEllipsized(2, y, display.width() - 4 - reserve, tmpl, sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
}
|
||||
});
|
||||
}
|
||||
return 2000;
|
||||
return (mq_delay > 0 && mq_delay < 2000) ? mq_delay : 2000;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
|
||||
@@ -771,6 +771,7 @@ public:
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
int mq_delay = 0; // >0 while the selected row's name is marquee-scrolling
|
||||
|
||||
// Periodic refresh of the selected entry while in detail or navigate view,
|
||||
// preserving the selection across the list rebuild. Navigate refreshes
|
||||
@@ -876,7 +877,8 @@ public:
|
||||
if (_source == SRC_SCAN && !e.name[0]) { // unknown node → "[Type]"
|
||||
snprintf(filt, sizeof(filt), "[%s]", typeName(e.type));
|
||||
}
|
||||
display.drawTextEllipsized(tx, y, dist_col - tx - 2, filt);
|
||||
int mqr = display.drawTextEllipsized(tx, y, dist_col - tx - 2, filt, sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
|
||||
display.setColor(sel ? DisplayDriver::DARK : DisplayDriver::LIGHT);
|
||||
char right[10];
|
||||
@@ -894,8 +896,10 @@ public:
|
||||
}
|
||||
|
||||
if (renderActivePopup(display)) return 50;
|
||||
if (_source == SRC_SCAN) return _scanning ? 200 : 2000;
|
||||
return _count == 0 ? 3000 : 2000;
|
||||
int ret;
|
||||
if (_source == SRC_SCAN) ret = _scanning ? 200 : 2000;
|
||||
else ret = _count == 0 ? 3000 : 2000;
|
||||
return (mq_delay > 0 && mq_delay < ret) ? mq_delay : ret;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
|
||||
@@ -114,7 +114,9 @@ struct PopupMenu {
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
display.drawTextEllipsized(bx + pad, py, text_w, _items[idx]);
|
||||
// Return value not needed here: this popup already redraws every 50ms
|
||||
// (below), faster than any marquee step, so the animation is already smooth.
|
||||
display.drawTextEllipsized(bx + pad, py, text_w, _items[idx], idx == _sel);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
|
||||
|
||||
@@ -440,8 +440,11 @@ class SettingsScreen : public UIScreen {
|
||||
return item - MSG_SLOT_0;
|
||||
}
|
||||
|
||||
void renderItem(DisplayDriver& display, int item, int y, bool sel) {
|
||||
// Returns 0, or the ms until the selected row's value should next redraw
|
||||
// to keep a marquee animation going (see DisplayDriver::drawTextEllipsized).
|
||||
int renderItem(DisplayDriver& display, int item, int y, bool sel) {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
int mq_delay = 0;
|
||||
|
||||
drawRowSelection(display, y, sel, _reserve);
|
||||
|
||||
@@ -514,7 +517,8 @@ class SettingsScreen : public UIScreen {
|
||||
display.print("Preset");
|
||||
const char* name = p ? _picker.currentName(p, radioTarget(p)) : "Custom";
|
||||
int xc = valCol(display);
|
||||
display.drawTextEllipsized(xc, y, display.width() - xc - _reserve, name);
|
||||
int r = display.drawTextEllipsized(xc, y, display.width() - xc - _reserve, name, sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
} else if (item == CUSTOM_FREQ) {
|
||||
display.print("Freq");
|
||||
int xc = valCol(display);
|
||||
@@ -559,8 +563,9 @@ class SettingsScreen : public UIScreen {
|
||||
} else if (item == SCOPE_NAME) {
|
||||
display.print("Scope");
|
||||
int vx = valCol(display);
|
||||
display.drawTextEllipsized(vx, y, display.width() - vx - _reserve,
|
||||
(p && p->default_scope_name[0]) ? p->default_scope_name : "(none)");
|
||||
int r = display.drawTextEllipsized(vx, y, display.width() - vx - _reserve,
|
||||
(p && p->default_scope_name[0]) ? p->default_scope_name : "(none)", sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
#if AUTO_OFF_MILLIS > 0
|
||||
} else if (item == AUTO_OFF) {
|
||||
display.print("AutoOff");
|
||||
@@ -596,7 +601,8 @@ class SettingsScreen : public UIScreen {
|
||||
} else if (item == DEVICE_NAME) {
|
||||
display.print("Name");
|
||||
int vx = valCol(display);
|
||||
display.drawTextEllipsized(vx, y, display.width() - vx - _reserve, the_mesh.getNodeName());
|
||||
int r = display.drawTextEllipsized(vx, y, display.width() - vx - _reserve, the_mesh.getNodeName(), sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
} else if (item == REBOOT) {
|
||||
display.print("Reboot"); // action row: Enter reboots this device
|
||||
} else if (item == KEYBOARD_TYPE) {
|
||||
@@ -677,8 +683,10 @@ 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 - _reserve, tmpl);
|
||||
int r = display.drawTextEllipsized(xm, y, display.width() - xm - _reserve, tmpl, sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
}
|
||||
return mq_delay;
|
||||
}
|
||||
|
||||
// Keyboard state for editing message slots
|
||||
@@ -722,6 +730,7 @@ public:
|
||||
|
||||
display.drawCenteredHeader("SETTINGS");
|
||||
|
||||
int mq_delay = 0;
|
||||
_acc.render(display,
|
||||
// Section header: "[+/-] Name"
|
||||
[&](int sec, int y, bool sel, int reserve, bool collapsed) {
|
||||
@@ -736,12 +745,13 @@ public:
|
||||
// Item row
|
||||
[&](int sec, int item, int y, bool sel, int reserve) {
|
||||
_reserve = reserve;
|
||||
renderItem(display, _sec_items[sec][item], y, sel);
|
||||
int r = renderItem(display, _sec_items[sec][item], y, sel);
|
||||
if (r > 0) mq_delay = r;
|
||||
});
|
||||
|
||||
if (_picker.menu.active) _picker.menu.render(display);
|
||||
|
||||
return 2000;
|
||||
return (mq_delay > 0 && mq_delay < 2000) ? mq_delay : 2000;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
|
||||
@@ -718,6 +718,7 @@ public:
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
char tmp[80];
|
||||
int mq_delay = 0; // >0 while a selected row's name is marquee-scrolling
|
||||
display.setTextSize(1);
|
||||
const int lh = display.getLineHeight(); // line height at sz1
|
||||
const int step = display.lineStep(); // lh + 2
|
||||
@@ -1198,7 +1199,8 @@ public:
|
||||
int name_y = cy + (cell_h - line_h) / 2;
|
||||
int name_max_w = cell_w - 4 - bw;
|
||||
if (name_max_w < 6) name_max_w = 6;
|
||||
display.drawTextEllipsized(cx + 2, name_y, name_max_w, name);
|
||||
int r = display.drawTextEllipsized(cx + 2, name_y, name_max_w, name, sel);
|
||||
if (sel && r > 0) mq_delay = r;
|
||||
if (unread > 0)
|
||||
display.drawUnreadBadge(cx + cell_w - 2, name_y, unread, sel);
|
||||
} else {
|
||||
@@ -1244,13 +1246,15 @@ public:
|
||||
(auto_adv || _task->trail().isActive() || repeating || loc_sharing);
|
||||
if (Features::IS_EINK) {
|
||||
// slow display: poll every 30 s; inbound msgs force immediate refresh via notify()
|
||||
return Features::HOME_REFRESH_MS;
|
||||
return (mq_delay > 0 && mq_delay < Features::HOME_REFRESH_MS) ? mq_delay : Features::HOME_REFRESH_MS;
|
||||
}
|
||||
if (_page == HomePage::CLOCK) {
|
||||
bool show_sec = !_node_prefs || !_node_prefs->clock_hide_seconds;
|
||||
return need_blink ? 1000 : (show_sec ? 1000 : 60000);
|
||||
int ret = need_blink ? 1000 : (show_sec ? 1000 : 60000);
|
||||
return (mq_delay > 0 && mq_delay < ret) ? mq_delay : ret;
|
||||
}
|
||||
return need_blink ? 1000 : 5000;
|
||||
int ret = need_blink ? 1000 : 5000;
|
||||
return (mq_delay > 0 && mq_delay < ret) ? mq_delay : ret;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
|
||||
@@ -190,7 +190,10 @@ class WaypointsView {
|
||||
display.setCursor(2, top + 2 * step); display.print("Cancel to abort");
|
||||
}
|
||||
|
||||
void renderWpList(DisplayDriver& display) {
|
||||
// Returns 0, or the ms until the selected row's name should next redraw to
|
||||
// keep a marquee animation going (see DisplayDriver::drawTextEllipsized).
|
||||
int renderWpList(DisplayDriver& display) {
|
||||
int mq_delay = 0;
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
char title[24];
|
||||
snprintf(title, sizeof(title), "WAYPOINTS %d/%d",
|
||||
@@ -219,10 +222,12 @@ class WaypointsView {
|
||||
}
|
||||
char nm[24];
|
||||
display.translateUTF8ToBlocks(nm, label, sizeof(nm));
|
||||
display.drawTextEllipsized(2, y, display.width() - 2 - bw - reserve, nm);
|
||||
int mqr = display.drawTextEllipsized(2, y, display.width() - 2 - bw - reserve, nm, sel);
|
||||
if (sel && mqr > 0) mq_delay = mqr;
|
||||
if (dist[0]) { display.setCursor(display.width() - bw + 1 - reserve, y); display.print(dist); }
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
});
|
||||
return mq_delay;
|
||||
}
|
||||
|
||||
void renderWpNav(DisplayDriver& display) {
|
||||
@@ -361,9 +366,9 @@ public:
|
||||
if (_mode == AVG) { renderAvg(display); return display.isEink() ? 1000 : 300; }
|
||||
if (_mode == TRACKBACK) { renderTrackBack(display); return 1000; }
|
||||
if (_mode == NAV) { renderWpNav(display); return 1000; }
|
||||
renderWpList(display); // LIST
|
||||
int mq_delay = renderWpList(display); // LIST
|
||||
if (_ctx.active) _ctx.render(display);
|
||||
return 1000;
|
||||
return (mq_delay > 0 && mq_delay < 1000) ? mq_delay : 1000;
|
||||
}
|
||||
|
||||
// Returns true if the input was consumed (always, while active()).
|
||||
|
||||
@@ -2,12 +2,24 @@
|
||||
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <Arduino.h>
|
||||
|
||||
class DisplayDriver {
|
||||
int _w, _h;
|
||||
protected:
|
||||
bool _vw_dirty = true;
|
||||
bool _vw_result = false;
|
||||
|
||||
// ---- Marquee state for the single currently-selected overflowing label ----
|
||||
// Only one row/label can be "selected" at a time across the whole UI, so one
|
||||
// slot of state (rather than per-caller) is enough, and keeps every call
|
||||
// site down to passing a bool.
|
||||
char _marquee_text[64] = {0};
|
||||
int _marquee_max_w = -1;
|
||||
uint16_t _marquee_skip_cp = 0;
|
||||
uint16_t _marquee_max_skip_cp = 0;
|
||||
uint8_t _marquee_phase = 0; // 0=hold@start 1=scroll fwd 2=hold@end 3=scroll back
|
||||
unsigned long _marquee_next_at = 0;
|
||||
DisplayDriver(int w, int h) { _w = w; _h = h; }
|
||||
void setDimensions(int w, int h) { _w = w; _h = h; }
|
||||
public:
|
||||
@@ -320,8 +332,24 @@ public:
|
||||
}
|
||||
|
||||
|
||||
// draw text with ellipsis if it exceeds max_width
|
||||
virtual void drawTextEllipsized(int x, int y, int max_width, const char* str) {
|
||||
// Marquee timing — e-ink gets slower, coarser steps (fewer, cheaper partial
|
||||
// refreshes; a fine pixel/char scroll would be both slow and prone to
|
||||
// ghosting there). Unchanged frames are skipped by the display's own CRC
|
||||
// diff before any real panel push happens, so the hold phases are free.
|
||||
virtual unsigned long marqueeStepMs() { return isEink() ? 900 : 220; }
|
||||
virtual unsigned long marqueeHoldMs() { return isEink() ? 2500 : 1500; }
|
||||
virtual uint8_t marqueeStepChars() { return isEink() ? 3 : 1; }
|
||||
|
||||
// draw text with ellipsis if it exceeds max_width. Pass selected=true for
|
||||
// the row currently highlighted/focused by the user: instead of a static
|
||||
// ellipsis, an overflowing label then animates a "swing" marquee — holds at
|
||||
// the start, scrolls to reveal the full tail, holds there, scrolls back to
|
||||
// the start, and repeats for as long as the caller keeps passing
|
||||
// selected=true for this same text.
|
||||
// Returns 0 if nothing is animating (the caller's normal redraw cadence is
|
||||
// fine), or the number of ms until the next animation step is due — screens
|
||||
// clamp their render() return value to this so the marquee stays smooth.
|
||||
virtual int drawTextEllipsized(int x, int y, int max_width, const char* str, bool selected = false) {
|
||||
char temp_str[256]; // reasonable buffer size
|
||||
translateUTF8ToBlocks(temp_str, str, sizeof(temp_str));
|
||||
|
||||
@@ -333,12 +361,14 @@ public:
|
||||
// measures the same, so the width/ellipsis maths below is unaffected.
|
||||
for (char* q = temp_str; *q; q++) if (*q == '\n' || *q == '\r') *q = ' ';
|
||||
|
||||
if (getTextWidth(temp_str) <= max_width) {
|
||||
int full_width = getTextWidth(temp_str);
|
||||
if (full_width <= max_width) {
|
||||
setCursor(x, y);
|
||||
print(temp_str);
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!selected) {
|
||||
// for variable-width fonts (GxEPD), add space after ellipsis
|
||||
// for fixed-width fonts (OLED), keep tight spacing to save precious characters
|
||||
const char* ellipsis;
|
||||
@@ -367,6 +397,84 @@ public:
|
||||
|
||||
setCursor(x, y);
|
||||
print(temp_str);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- marquee (selected + overflowing) ----
|
||||
unsigned long now = millis();
|
||||
bool is_new = (strcmp(temp_str, _marquee_text) != 0) || (max_width != _marquee_max_w);
|
||||
if (is_new) {
|
||||
strncpy(_marquee_text, temp_str, sizeof(_marquee_text) - 1);
|
||||
_marquee_text[sizeof(_marquee_text) - 1] = 0;
|
||||
_marquee_max_w = max_width;
|
||||
_marquee_skip_cp = 0;
|
||||
_marquee_phase = 0; // hold at start
|
||||
_marquee_next_at = now + marqueeHoldMs();
|
||||
|
||||
// Find the codepoint skip at which the remaining suffix's own width
|
||||
// already fits max_width — i.e. the fully-scrolled end position.
|
||||
const uint8_t* p = (const uint8_t*)temp_str;
|
||||
int removed_w = 0;
|
||||
uint16_t cp_count = 0;
|
||||
while (*p) {
|
||||
uint32_t cp = decodeCodepoint(p);
|
||||
removed_w += getCodepointWidth(cp);
|
||||
cp_count++;
|
||||
if (full_width - removed_w <= max_width) break;
|
||||
}
|
||||
_marquee_max_skip_cp = cp_count;
|
||||
}
|
||||
|
||||
// Advance the state machine at most once per elapsed step/hold interval.
|
||||
if ((int32_t)(now - _marquee_next_at) >= 0) {
|
||||
uint8_t step = marqueeStepChars();
|
||||
switch (_marquee_phase) {
|
||||
case 0: // was holding at start -> begin scrolling forward
|
||||
case 1: // scrolling forward
|
||||
_marquee_skip_cp += step;
|
||||
_marquee_phase = 1;
|
||||
if (_marquee_skip_cp >= _marquee_max_skip_cp) {
|
||||
_marquee_skip_cp = _marquee_max_skip_cp;
|
||||
_marquee_phase = 2;
|
||||
_marquee_next_at = now + marqueeHoldMs();
|
||||
} else {
|
||||
_marquee_next_at = now + marqueeStepMs();
|
||||
}
|
||||
break;
|
||||
case 2: // was holding at end -> begin scrolling back
|
||||
case 3: // scrolling back
|
||||
_marquee_phase = 3;
|
||||
if (_marquee_skip_cp <= step) {
|
||||
_marquee_skip_cp = 0;
|
||||
_marquee_phase = 0;
|
||||
_marquee_next_at = now + marqueeHoldMs();
|
||||
} else {
|
||||
_marquee_skip_cp -= step;
|
||||
_marquee_next_at = now + marqueeStepMs();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Render the window starting _marquee_skip_cp codepoints in, trimmed from
|
||||
// the tail (same technique as the static ellipsis path, minus the "..."
|
||||
// suffix) until it fits max_width.
|
||||
const uint8_t* p = (const uint8_t*)temp_str;
|
||||
for (uint16_t i = 0; i < _marquee_skip_cp && *p; i++) decodeCodepoint(p);
|
||||
char window[256];
|
||||
strncpy(window, (const char*)p, sizeof(window) - 1);
|
||||
window[sizeof(window) - 1] = 0;
|
||||
int wlen = strlen(window);
|
||||
while (wlen > 0 && getTextWidth(window) > max_width) {
|
||||
window[--wlen] = 0;
|
||||
}
|
||||
while (wlen > 0 && ((uint8_t)window[wlen - 1] & 0xC0) == 0xC0) {
|
||||
window[--wlen] = 0;
|
||||
}
|
||||
setCursor(x, y);
|
||||
print(window);
|
||||
|
||||
return (int)(_marquee_next_at > now ? (_marquee_next_at - now) : 1);
|
||||
}
|
||||
|
||||
virtual void setBrightness(uint8_t level) { } // level 0-4 (min to max), no-op default
|
||||
|
||||
Reference in New Issue
Block a user