refactor: replace all hardcoded pixel positions with layout helpers

Add lineStep(), headerH(), listStart(), listVisible(), valCol() to
DisplayDriver so layout derives from getLineHeight() instead of fixed
OLED constants. Refactor all screens (SettingsScreen, NearbyScreen,
FullscreenMsgView, QuickMsgScreen, BotScreen, DashboardConfigScreen,
AutoAdvertScreen, RingtoneEditorScreen) and all HomeScreen pages plus
the lock screen and splash screen in UITask.cpp.

On OLED (lh=8) positions are unchanged; on landscape e-ink (lh=16) all
text and widgets now scale correctly to the 250×122 display.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-05-22 18:05:09 +02:00
co-authored by Claude Sonnet 4.6
parent 18867a85c0
commit 744f263932
10 changed files with 358 additions and 287 deletions
@@ -25,22 +25,27 @@ public:
int render(DisplayDriver& display) override { int render(DisplayDriver& display) override {
display.setTextSize(1); display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawTextCentered(display.width() / 2, 0, "AUTO-ADVERT"); int label_y = display.listStart();
display.fillRect(0, 10, display.width(), 1); int bar_y = label_y + display.lineStep();
int bar_h = display.lineStep();
int tip_y = bar_y + bar_h + display.lineStep();
display.setCursor(2, 14); display.drawTextCentered(display.width() / 2, 0, "AUTO-ADVERT");
display.fillRect(0, display.headerH() - 1, display.width(), 1);
display.setCursor(2, label_y);
display.print("Interval:"); display.print("Interval:");
int idx = currentIdx(); int idx = currentIdx();
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, 24, display.width(), 12); display.fillRect(0, bar_y, display.width(), bar_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.drawTextCentered(display.width() / 2, 25, OPT_LABELS[idx]); display.drawTextCentered(display.width() / 2, bar_y + 1, OPT_LABELS[idx]);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.setCursor(2, 40); display.setCursor(2, tip_y);
display.print("< > to change"); display.print("< > to change");
display.setCursor(2, 51); display.setCursor(2, tip_y + display.lineStep());
display.print("[Esc] to save"); display.print("[Esc] to save");
return 500; return 500;
} }
+12 -11
View File
@@ -8,9 +8,6 @@ class BotScreen : public UIScreen {
// Items: 0=Enable(DM), 1=Channel, 2=Trigger, 3=Reply DM, 4=Reply Ch // Items: 0=Enable(DM), 1=Channel, 2=Trigger, 3=Reply DM, 4=Reply Ch
static const int ITEM_COUNT = 5; static const int ITEM_COUNT = 5;
static const int ITEM_H = 11;
static const int START_Y = 12;
static const int VAL_X = 70;
int _sel; int _sel;
bool _dirty; bool _dirty;
@@ -58,20 +55,24 @@ public:
return _kb.render(display); return _kb.render(display);
} }
int item_h = display.lineStep();
int start_y = display.listStart();
int val_x = display.valCol();
display.drawTextCentered(display.width() / 2, 0, "AUTO-REPLY BOT"); display.drawTextCentered(display.width() / 2, 0, "AUTO-REPLY BOT");
display.fillRect(0, 10, display.width(), 1); display.fillRect(0, display.headerH() - 1, display.width(), 1);
static const char* labels[] = { "Enable", "Channel", "Trigger", "Reply DM", "Reply Ch" }; static const char* labels[] = { "Enable", "Channel", "Trigger", "Reply DM", "Reply Ch" };
for (int i = 0; i < ITEM_COUNT; i++) { for (int i = 0; i < ITEM_COUNT; i++) {
int y = START_Y + i * ITEM_H; int y = start_y + i * item_h;
bool sel = (i == _sel); bool sel = (i == _sel);
if (sel) { if (sel) {
display.fillRect(0, y - 1, display.width(), ITEM_H); display.fillRect(0, y - 1, display.width(), item_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} }
display.setCursor(2, y); display.setCursor(2, y);
display.print(labels[i]); display.print(labels[i]);
display.setCursor(VAL_X, y); display.setCursor(val_x, y);
if (i == 0) { if (i == 0) {
display.print(_prefs->bot_enabled ? "ON" : "OFF"); display.print(_prefs->bot_enabled ? "ON" : "OFF");
@@ -81,19 +82,19 @@ public:
} else { } else {
ChannelDetails ch; ChannelDetails ch;
if (the_mesh.getChannel(_prefs->bot_channel_idx, ch) && ch.name[0]) 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, ch.name);
else else
display.print("?"); display.print("?");
} }
} else if (i == 2) { } else if (i == 2) {
const char* tr = _prefs->bot_trigger; const char* tr = _prefs->bot_trigger;
display.drawTextEllipsized(VAL_X, y, display.width() - VAL_X - 1, tr[0] ? tr : "(none)"); display.drawTextEllipsized(val_x, y, display.width() - val_x - 1, tr[0] ? tr : "(none)");
} else if (i == 3) { } else if (i == 3) {
const char* rp = _prefs->bot_reply_dm; const char* rp = _prefs->bot_reply_dm;
display.drawTextEllipsized(VAL_X, y, display.width() - VAL_X - 1, rp[0] ? rp : "(none)"); display.drawTextEllipsized(val_x, y, display.width() - val_x - 1, rp[0] ? rp : "(none)");
} else { } else {
const char* rp = _prefs->bot_reply_ch; const char* rp = _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, rp[0] ? rp : "(none)");
} }
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
} }
@@ -21,9 +21,6 @@ class DashboardConfigScreen : public UIScreen {
NodePrefs* _prefs; NodePrefs* _prefs;
static const int FIELD_SLOTS = 3; static const int FIELD_SLOTS = 3;
static const int ITEM_H = 13;
static const int START_Y = 13;
static const int VAL_X = 62;
static const char* OPTION_NAMES[DASH_COUNT]; static const char* OPTION_NAMES[DASH_COUNT];
@@ -44,23 +41,27 @@ public:
int render(DisplayDriver& display) override { int render(DisplayDriver& display) override {
display.setTextSize(1); display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
int item_h = display.lineStep();
int start_y = display.listStart();
int val_x = display.valCol();
display.drawTextCentered(display.width() / 2, 0, "CLOCK FIELDS"); display.drawTextCentered(display.width() / 2, 0, "CLOCK FIELDS");
display.fillRect(0, 10, display.width(), 1); display.fillRect(0, display.headerH() - 1, display.width(), 1);
static const char* labels[] = { "Field 1", "Field 2", "Field 3" }; static const char* labels[] = { "Field 1", "Field 2", "Field 3" };
for (int i = 0; i < FIELD_SLOTS; i++) { for (int i = 0; i < FIELD_SLOTS; i++) {
int y = START_Y + i * ITEM_H; int y = start_y + i * item_h;
bool sel = (i == _sel); bool sel = (i == _sel);
if (sel) { if (sel) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y - 1, display.width(), ITEM_H); display.fillRect(0, y - 1, display.width(), item_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
} }
display.setCursor(2, y); display.setCursor(2, y);
display.print(labels[i]); display.print(labels[i]);
display.setCursor(VAL_X, y); display.setCursor(val_x, y);
uint8_t f = _prefs->dashboard_fields[i]; uint8_t f = _prefs->dashboard_fields[i];
display.print(OPTION_NAMES[f < DASH_COUNT ? f : DASH_NONE]); display.print(OPTION_NAMES[f < DASH_COUNT ? f : DASH_NONE]);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
@@ -82,7 +82,8 @@ struct FullscreenMsgView {
} }
} }
const int header_h = to_nick[0] ? 20 : 10; const int cw = display.getCharWidth();
const int header_h = to_nick[0] ? (lineH * 2 + 4) : (lineH + 2);
const int startY = header_h + 2; const int startY = header_h + 2;
const int visible = (display.height() - startY - lineH) / lineH; const int visible = (display.height() - startY - lineH) / lineH;
@@ -94,7 +95,7 @@ struct FullscreenMsgView {
char trans_nick[32], to_label[36]; char trans_nick[32], to_label[36];
display.translateUTF8ToBlocks(trans_nick, to_nick, sizeof(trans_nick)); display.translateUTF8ToBlocks(trans_nick, to_nick, sizeof(trans_nick));
snprintf(to_label, sizeof(to_label), "To: %s", trans_nick); snprintf(to_label, sizeof(to_label), "To: %s", trans_nick);
display.drawTextEllipsized(2, 11, display.width() - 4, to_label); display.drawTextEllipsized(2, lineH + 3, display.width() - 4, to_label);
} }
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
@@ -111,16 +112,16 @@ struct FullscreenMsgView {
} }
if (scroll > 0) { if (scroll > 0) {
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.fillRect(display.width() - 6, startY, 6, lineH); display.fillRect(display.width() - cw, startY, cw, lineH);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.setCursor(display.width() - 6, startY); display.setCursor(display.width() - cw, startY);
display.print("^"); display.print("^");
} }
if (scroll < max_scroll) { if (scroll < max_scroll) {
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.fillRect(display.width() - 6, startY + (visible - 1) * lineH, 6, lineH); display.fillRect(display.width() - cw, startY + (visible - 1) * lineH, cw, lineH);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.setCursor(display.width() - 6, startY + (visible - 1) * lineH); display.setCursor(display.width() - cw, startY + (visible - 1) * lineH);
display.print("v"); display.print("v");
} }
const int nav_y = display.height() - lineH; const int nav_y = display.height() - lineH;
@@ -129,7 +130,7 @@ struct FullscreenMsgView {
display.print("<"); display.print("<");
} }
if (has_prev) { if (has_prev) {
display.setCursor(display.width() - 6, nav_y); display.setCursor(display.width() - cw, nav_y);
display.print(">"); display.print(">");
} }
return 2000; return 2000;
+65 -52
View File
@@ -8,10 +8,7 @@
class NearbyScreen : public UIScreen { class NearbyScreen : public UIScreen {
UITask* _task; UITask* _task;
static const int VISIBLE = 4; int _visible = 4; // updated each render; used by handleInput for scroll clamping
static const int ITEM_H = 12;
static const int START_Y = 12;
static const int DIST_COL = 86;
static const int FILTER_COUNT = 6; static const int FILTER_COUNT = 6;
static const char* FILTER_LABELS[FILTER_COUNT]; static const char* FILTER_LABELS[FILTER_COUNT];
@@ -54,10 +51,7 @@ class NearbyScreen : public UIScreen {
int _dsel; int _dsel;
bool _ddetail; bool _ddetail;
static const int D_BOX_H = 19; int _d_visible = 2; // updated each render; used by handleInputDiscover
static const int D_ITEM_H = 21;
static const int D_VISIBLE = 2;
static const int D_START_Y = 11;
// ── helpers ────────────────────────────────────────────────────────────────── // ── helpers ──────────────────────────────────────────────────────────────────
static void pubKeyToBase64(const uint8_t* key, char* out, int out_len) { static void pubKeyToBase64(const uint8_t* key, char* out, int out_len) {
@@ -193,6 +187,14 @@ class NearbyScreen : public UIScreen {
} }
int renderDiscover(DisplayDriver& display) { int renderDiscover(DisplayDriver& display) {
int lh = display.getLineHeight();
int hdr = display.headerH();
int d_box_h = 2 * lh + 3; // two text rows + padding
int d_item_h = d_box_h + 2;
int d_start_y = hdr;
_d_visible = display.listVisible(d_item_h);
if (_d_visible < 1) _d_visible = 1;
if (_ddetail) { if (_ddetail) {
// ── full-screen detail for selected node ────────────────────────────── // ── full-screen detail for selected node ──────────────────────────────
const DiscoverResult& r = _dresults[_dsel]; const DiscoverResult& r = _dresults[_dsel];
@@ -207,29 +209,29 @@ class NearbyScreen : public UIScreen {
char filtered[32]; char filtered[32];
display.translateUTF8ToBlocks(filtered, label, sizeof(filtered)); display.translateUTF8ToBlocks(filtered, label, sizeof(filtered));
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, 0, display.width(), 10); display.fillRect(0, 0, display.width(), hdr - 1);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.drawTextEllipsized(2, 1, display.width() - 4, filtered); display.drawTextEllipsized(2, 1, display.width() - 4, filtered);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, 10, display.width(), 1); display.fillRect(0, hdr - 1, display.width(), 1);
// public key as base64, truncated with ... by drawTextEllipsized // public key as base64
char b64[48]; char b64[48];
pubKeyToBase64(r.pub_key, b64, sizeof(b64)); pubKeyToBase64(r.pub_key, b64, sizeof(b64));
display.drawTextEllipsized(2, 12, display.width() - 4, b64); display.drawTextEllipsized(2, hdr, display.width() - 4, b64);
int step = lh + 1;
char buf[32]; char buf[32];
snprintf(buf, sizeof(buf), "RSSI: %d dBm", (int)r.rssi); snprintf(buf, sizeof(buf), "RSSI: %d dBm", (int)r.rssi);
display.setCursor(2, 21); display.print(buf); display.setCursor(2, hdr + step); display.print(buf);
snprintf(buf, sizeof(buf), "SNR: %d dB", (int)(r.snr_x4 / 4)); snprintf(buf, sizeof(buf), "SNR: %d dB", (int)(r.snr_x4 / 4));
display.setCursor(2, 30); display.print(buf); display.setCursor(2, hdr + step * 2); display.print(buf);
snprintf(buf, sizeof(buf), "Rem: %d dB", (int)(r.remote_snr_x4 / 4)); snprintf(buf, sizeof(buf), "Rem: %d dB", (int)(r.remote_snr_x4 / 4));
display.setCursor(2, 39); display.print(buf); display.setCursor(2, hdr + step * 3); display.print(buf);
display.setCursor(2, 48); display.setCursor(2, hdr + step * 4);
display.print(r.is_known ? "Status: known" : "Status: new"); display.print(r.is_known ? "Status: known" : "Status: new");
return 5000; return 5000;
@@ -250,21 +252,22 @@ class NearbyScreen : public UIScreen {
else else
snprintf(title, sizeof(title), "DISCOVER (%d found)", _dresult_count); snprintf(title, sizeof(title), "DISCOVER (%d found)", _dresult_count);
display.drawTextCentered(display.width() / 2, 0, title); display.drawTextCentered(display.width() / 2, 0, title);
display.fillRect(0, 10, display.width(), 1); display.fillRect(0, hdr - 1, display.width(), 1);
if (_dresult_count == 0) { if (_dresult_count == 0) {
display.drawTextCentered(display.width() / 2, 32, display.drawTextCentered(display.width() / 2, display.height() / 2,
_discovering ? "Waiting for replies..." : "No nodes found"); _discovering ? "Waiting for replies..." : "No nodes found");
} else { } else {
if (_dsel >= _dresult_count) _dsel = _dresult_count - 1; if (_dsel >= _dresult_count) _dsel = _dresult_count - 1;
if (_dscroll > _dresult_count - D_VISIBLE) if (_dscroll > _dresult_count - _d_visible)
_dscroll = _dresult_count > D_VISIBLE ? _dresult_count - D_VISIBLE : 0; _dscroll = _dresult_count > _d_visible ? _dresult_count - _d_visible : 0;
if (_dscroll < 0) _dscroll = 0; if (_dscroll < 0) _dscroll = 0;
for (int i = 0; i < D_VISIBLE && (_dscroll + i) < _dresult_count; i++) {
for (int i = 0; i < _d_visible && (_dscroll + i) < _dresult_count; i++) {
int idx = _dscroll + i; int idx = _dscroll + i;
bool sel = (idx == _dsel); bool sel = (idx == _dsel);
const DiscoverResult& r = _dresults[idx]; const DiscoverResult& r = _dresults[idx];
int y = D_START_Y + i * D_ITEM_H; int y = d_start_y + i * d_item_h;
const char* typeStr = (r.type == ADV_TYPE_REPEATER) ? "Rpt" : const char* typeStr = (r.type == ADV_TYPE_REPEATER) ? "Rpt" :
(r.type == ADV_TYPE_SENSOR) ? "Snsr" : (r.type == ADV_TYPE_SENSOR) ? "Snsr" :
@@ -272,11 +275,11 @@ class NearbyScreen : public UIScreen {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
if (sel) { if (sel) {
display.fillRect(0, y, display.width(), D_BOX_H); // fully inverted when selected display.fillRect(0, y, display.width(), d_box_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} else { } else {
display.drawRect(0, y, display.width(), D_BOX_H); display.drawRect(0, y, display.width(), d_box_h);
display.fillRect(1, y + 1, display.width() - 2, 8); display.fillRect(1, y + 1, display.width() - 2, lh);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} }
@@ -300,14 +303,15 @@ class NearbyScreen : public UIScreen {
display.setColor(sel ? DisplayDriver::DARK : DisplayDriver::LIGHT); display.setColor(sel ? DisplayDriver::DARK : DisplayDriver::LIGHT);
char sig[24]; char sig[24];
snprintf(sig, sizeof(sig), "RSSI:%d SNR:%d", (int)r.rssi, (int)(r.snr_x4 / 4)); snprintf(sig, sizeof(sig), "RSSI:%d SNR:%d", (int)r.rssi, (int)(r.snr_x4 / 4));
display.drawTextEllipsized(3, y + 10, display.width() - 6, sig); display.drawTextEllipsized(3, y + lh + 2, display.width() - 6, sig);
} }
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
int cw = display.getCharWidth();
if (_dscroll > 0) if (_dscroll > 0)
{ display.setCursor(display.width() - 6, D_START_Y + 1); display.print("^"); } { display.setCursor(display.width() - cw, d_start_y); display.print("^"); }
if (_dscroll + D_VISIBLE < _dresult_count) if (_dscroll + _d_visible < _dresult_count)
{ display.setCursor(display.width() - 6, D_START_Y + D_VISIBLE * D_ITEM_H - 10); display.print("v"); } { display.setCursor(display.width() - cw, d_start_y + (_d_visible - 1) * d_item_h); display.print("v"); }
} }
return _discovering ? 200 : 2000; return _discovering ? 200 : 2000;
@@ -342,7 +346,7 @@ class NearbyScreen : public UIScreen {
} }
if (c == KEY_DOWN && _dsel < _dresult_count - 1) { if (c == KEY_DOWN && _dsel < _dresult_count - 1) {
_dsel++; _dsel++;
if (_dsel >= _dscroll + D_VISIBLE) _dscroll = _dsel - D_VISIBLE + 1; if (_dsel >= _dscroll + _d_visible) _dscroll = _dsel - _d_visible + 1;
return true; return true;
} }
return true; return true;
@@ -390,9 +394,12 @@ public:
// ── detail view ────────────────────────────────────────────────────────── // ── detail view ──────────────────────────────────────────────────────────
if (_detail && _sel < _count) { if (_detail && _sel < _count) {
const Entry& e = _entries[_sel]; const Entry& e = _entries[_sel];
int lh = display.getLineHeight();
int hdr = display.headerH();
int step = lh + 1;
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, 0, display.width(), 10); display.fillRect(0, 0, display.width(), hdr - 1);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
char filtered[32]; char filtered[32];
display.translateUTF8ToBlocks(filtered, e.name, sizeof(filtered)); display.translateUTF8ToBlocks(filtered, e.name, sizeof(filtered));
@@ -401,55 +408,60 @@ public:
char buf[32]; char buf[32];
snprintf(buf, sizeof(buf), "Lat: %.5f", e.lat_e6 / 1e6); snprintf(buf, sizeof(buf), "Lat: %.5f", e.lat_e6 / 1e6);
display.setCursor(2, 11); display.print(buf); display.setCursor(2, hdr); display.print(buf);
snprintf(buf, sizeof(buf), "Lon: %.5f", e.lon_e6 / 1e6); snprintf(buf, sizeof(buf), "Lon: %.5f", e.lon_e6 / 1e6);
display.setCursor(2, 20); display.print(buf); display.setCursor(2, hdr + step); display.print(buf);
if (e.dist_km >= 0.0f) { if (e.dist_km >= 0.0f) {
char dist[12]; char dist[12];
fmtDist(dist, sizeof(dist), e.dist_km); fmtDist(dist, sizeof(dist), e.dist_km);
int az = bearingDeg(_own_lat, _own_lon, e.lat_e6, e.lon_e6); int az = bearingDeg(_own_lat, _own_lon, e.lat_e6, e.lon_e6);
snprintf(buf, sizeof(buf), "Dist: %s", dist); snprintf(buf, sizeof(buf), "Dist: %s", dist);
display.setCursor(2, 29); display.print(buf); display.setCursor(2, hdr + step * 2); display.print(buf);
snprintf(buf, sizeof(buf), "Az: %dd (%s)", az, bearingCardinal(az)); snprintf(buf, sizeof(buf), "Az: %dd (%s)", az, bearingCardinal(az));
display.setCursor(2, 38); display.print(buf); display.setCursor(2, hdr + step * 3); display.print(buf);
} else { } else {
display.setCursor(2, 29); display.print("Dist: no own GPS"); display.setCursor(2, hdr + step * 2); display.print("Dist: no own GPS");
display.setCursor(2, 38); display.print("Az: unknown"); display.setCursor(2, hdr + step * 3); display.print("Az: unknown");
} }
snprintf(buf, sizeof(buf), "Type: %s", typeName(e.type)); snprintf(buf, sizeof(buf), "Type: %s", typeName(e.type));
display.setCursor(2, 47); display.print(buf); display.setCursor(2, hdr + step * 4); display.print(buf);
char age[16]; char age[16];
fmtAge(age, sizeof(age), e.lastmod); fmtAge(age, sizeof(age), e.lastmod);
snprintf(buf, sizeof(buf), "Seen: %s", age); snprintf(buf, sizeof(buf), "Seen: %s", age);
display.setCursor(2, 56); display.print(buf); display.setCursor(2, hdr + step * 5); display.print(buf);
return 2000; return 2000;
} }
// ── list view ──────────────────────────────────────────────────────────── // ── list view ────────────────────────────────────────────────────────────
int item_h = display.lineStep();
int start_y = display.listStart();
int dist_col = display.width() - display.getCharWidth() * 7;
_visible = display.listVisible(item_h);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
char title[22]; char title[22];
snprintf(title, sizeof(title), "NEARBY[%s]", FILTER_LABELS[_filter]); snprintf(title, sizeof(title), "NEARBY[%s]", FILTER_LABELS[_filter]);
display.drawTextCentered(display.width() / 2, 0, title); display.drawTextCentered(display.width() / 2, 0, title);
display.fillRect(0, 10, display.width(), 1); display.fillRect(0, display.headerH() - 1, display.width(), 1);
if (_count == 0) { if (_count == 0) {
display.drawTextCentered(display.width() / 2, 28, "No contacts found"); display.drawTextCentered(display.width() / 2, display.height() / 2 - display.lineStep() / 2, "No contacts found");
display.drawTextCentered(display.width() / 2, 40, "[Enter]=Discover"); display.drawTextCentered(display.width() / 2, display.height() / 2 + display.lineStep() / 2, "[Enter]=Discover");
} else { } else {
for (int i = 0; i < VISIBLE && (_scroll + i) < _count; i++) { for (int i = 0; i < _visible && (_scroll + i) < _count; i++) {
int idx = _scroll + i; int idx = _scroll + i;
bool sel = (idx == _sel); bool sel = (idx == _sel);
int y = START_Y + i * ITEM_H; int y = start_y + i * item_h;
const Entry& e = _entries[idx]; const Entry& e = _entries[idx];
if (sel) { if (sel) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y - 1, display.width(), ITEM_H - 1); display.fillRect(0, y - 1, display.width(), item_h - 1);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
@@ -457,21 +469,22 @@ public:
char filt[32]; char filt[32];
display.translateUTF8ToBlocks(filt, e.name, sizeof(filt)); display.translateUTF8ToBlocks(filt, e.name, sizeof(filt));
display.drawTextEllipsized(2, y, DIST_COL - 4, filt); display.drawTextEllipsized(2, y, dist_col - 4, filt);
display.setColor(sel ? DisplayDriver::DARK : DisplayDriver::LIGHT); display.setColor(sel ? DisplayDriver::DARK : DisplayDriver::LIGHT);
char dist[10]; char dist[10];
if (e.dist_km >= 0.0f) fmtDist(dist, sizeof(dist), e.dist_km); if (e.dist_km >= 0.0f) fmtDist(dist, sizeof(dist), e.dist_km);
else strncpy(dist, "?GPS", sizeof(dist)); else strncpy(dist, "?GPS", sizeof(dist));
display.setCursor(DIST_COL, y); display.setCursor(dist_col, y);
display.print(dist); display.print(dist);
} }
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
int cw = display.getCharWidth();
if (_scroll > 0) if (_scroll > 0)
{ display.setCursor(display.width() - 6, START_Y); display.print("^"); } { display.setCursor(display.width() - cw, start_y); display.print("^"); }
if (_scroll + VISIBLE < _count) if (_scroll + _visible < _count)
{ display.setCursor(display.width() - 6, START_Y + (VISIBLE - 1) * ITEM_H); display.print("v"); } { display.setCursor(display.width() - cw, start_y + (_visible - 1) * item_h); display.print("v"); }
} }
if (_ctx_menu.active) { if (_ctx_menu.active) {
@@ -519,7 +532,7 @@ public:
} }
if (c == KEY_DOWN && _sel < _count - 1) { if (c == KEY_DOWN && _sel < _count - 1) {
_sel++; _sel++;
if (_sel >= _scroll + VISIBLE) _scroll = _sel - VISIBLE + 1; if (_sel >= _scroll + _visible) _scroll = _sel - _visible + 1;
return true; return true;
} }
if (c == KEY_ENTER && _count == 0) { enterDiscoverMode(); return true; } if (c == KEY_ENTER && _count == 0) { enterDiscoverMode(); return true; }
@@ -61,13 +61,8 @@ class QuickMsgScreen : public UIScreen {
int _dm_hist_sel, _dm_hist_scroll; int _dm_hist_sel, _dm_hist_scroll;
FullscreenMsgView _dm_fs; FullscreenMsgView _dm_fs;
static const int VISIBLE = 4; int _visible = 4; // updated in render(); used by handleInput() for scroll clamping
static const int ITEM_H = 12; int _hist_visible = 2; // updated in render(); for history list scroll clamping
static const int START_Y = 12;
static const int HIST_VISIBLE = 2; // 2-line boxed history entries
static const int HIST_ITEM_H = 21; // box(19) + gap(2)
static const int HIST_BOX_H = 19;
static const int HIST_START_Y = 11;
void expandMsg(const char* tmpl, char* out, int out_len) const { void expandMsg(const char* tmpl, char* out, int out_len) const {
double lat = 0, lon = 0; double lat = 0, lon = 0;
@@ -128,17 +123,6 @@ class QuickMsgScreen : public UIScreen {
} }
} }
void renderScrollHints(DisplayDriver& display, int scroll, int count) {
display.setColor(DisplayDriver::LIGHT);
if (scroll > 0) {
display.setCursor(display.width() - 6, START_Y);
display.print("^");
}
if (scroll + VISIBLE < count) {
display.setCursor(display.width() - 6, START_Y + (VISIBLE-1)*ITEM_H);
display.print("v");
}
}
static void fmtMsgAge(char* buf, int n, uint32_t timestamp) { static void fmtMsgAge(char* buf, int n, uint32_t timestamp) {
uint32_t now = rtc_clock.getCurrentTime(); uint32_t now = rtc_clock.getCurrentTime();
@@ -476,9 +460,15 @@ public:
display.setTextSize(1); display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
int lh = display.getLineHeight();
int item_h = display.lineStep();
int start_y = display.listStart();
int cw = display.getCharWidth();
_visible = display.listVisible(item_h);
if (_phase == MODE_SELECT) { if (_phase == MODE_SELECT) {
display.drawTextCentered(display.width()/2, 0, "MESSAGE"); display.drawTextCentered(display.width()/2, 0, "MESSAGE");
display.fillRect(0, 10, display.width(), 1); display.fillRect(0, display.headerH() - 1, display.width(), 1);
const char* opts[] = { "Direct message", "Channels", "Room Servers" }; const char* opts[] = { "Direct message", "Channels", "Room Servers" };
int badges[3] = { int badges[3] = {
getDMUnreadTotal(), getDMUnreadTotal(),
@@ -486,18 +476,18 @@ public:
_task->getRoomUnreadCount() _task->getRoomUnreadCount()
}; };
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
int y = START_Y + i * ITEM_H; int y = start_y + i * item_h;
bool sel = (i == _mode_sel); bool sel = (i == _mode_sel);
if (sel) { if (sel) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y - 1, display.width(), ITEM_H - 1); display.fillRect(0, y - 1, display.width(), item_h - 1);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
} }
display.setCursor(0, y); display.setCursor(0, y);
display.print(sel ? ">" : " "); display.print(sel ? ">" : " ");
display.setCursor(8, y); display.setCursor(cw + 2, y);
display.print(opts[i]); display.print(opts[i]);
if (badges[i] > 0) { if (badges[i] > 0) {
char badge[5]; char badge[5];
@@ -511,22 +501,22 @@ public:
} else if (_phase == CONTACT_PICK) { } else if (_phase == CONTACT_PICK) {
display.drawTextCentered(display.width()/2, 0, _room_mode ? "SELECT ROOM" : "SELECT CONTACT"); display.drawTextCentered(display.width()/2, 0, _room_mode ? "SELECT ROOM" : "SELECT CONTACT");
display.fillRect(0, 10, display.width(), 1); display.fillRect(0, display.headerH() - 1, display.width(), 1);
if (_num_contacts == 0) { if (_num_contacts == 0) {
display.drawTextCentered(display.width()/2, 32, _room_mode ? "No room servers" : "No favourites"); display.drawTextCentered(display.width()/2, display.height()/2, _room_mode ? "No room servers" : "No favourites");
return 5000; return 5000;
} }
for (int i = 0; i < VISIBLE && (_contact_scroll+i) < _num_contacts; i++) { for (int i = 0; i < _visible && (_contact_scroll+i) < _num_contacts; i++) {
int list_idx = _contact_scroll + i; int list_idx = _contact_scroll + i;
int mesh_idx = _sorted[list_idx]; int mesh_idx = _sorted[list_idx];
bool sel = (list_idx == _contact_sel); bool sel = (list_idx == _contact_sel);
int y = START_Y + i * ITEM_H; int y = start_y + i * item_h;
if (sel) { if (sel) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y - 1, display.width(), ITEM_H - 1); display.fillRect(0, y - 1, display.width(), item_h - 1);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
@@ -545,7 +535,7 @@ public:
snprintf(badge, sizeof(badge), "%d", (int)dm_unread); snprintf(badge, sizeof(badge), "%d", (int)dm_unread);
bw = display.getTextWidth(badge) + 2; bw = display.getTextWidth(badge) + 2;
} }
display.drawTextEllipsized(8, y, display.width() - 8 - bw - 1, filtered); display.drawTextEllipsized(cw + 2, y, display.width() - cw - 2 - bw - 1, filtered);
if (dm_unread > 0) { if (dm_unread > 0) {
display.setCursor(display.width() - bw + 1, y); display.setCursor(display.width() - bw + 1, y);
display.print(badge); display.print(badge);
@@ -553,28 +543,29 @@ public:
} }
} }
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
renderScrollHints(display, _contact_scroll, _num_contacts); if (_contact_scroll > 0) { display.setCursor(display.width() - cw, start_y); display.print("^"); }
if (_contact_scroll + _visible < _num_contacts) { display.setCursor(display.width() - cw, start_y + (_visible-1)*item_h); display.print("v"); }
// Context menu overlay // Context menu overlay
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.drawTextCentered(display.width()/2, 0, "SELECT CHANNEL"); display.drawTextCentered(display.width()/2, 0, "SELECT CHANNEL");
display.fillRect(0, 10, display.width(), 1); display.fillRect(0, display.headerH() - 1, display.width(), 1);
if (_num_channels == 0) { if (_num_channels == 0) {
display.drawTextCentered(display.width()/2, 32, "No channels"); display.drawTextCentered(display.width()/2, display.height()/2, "No channels");
return 5000; return 5000;
} }
for (int i = 0; i < VISIBLE && (_channel_scroll+i) < _num_channels; i++) { for (int i = 0; i < _visible && (_channel_scroll+i) < _num_channels; i++) {
int list_idx = _channel_scroll + i; int list_idx = _channel_scroll + i;
bool sel = (list_idx == _channel_sel); bool sel = (list_idx == _channel_sel);
int y = START_Y + i * ITEM_H; int y = start_y + i * item_h;
if (sel) { if (sel) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y - 1, display.width(), ITEM_H - 1); display.fillRect(0, y - 1, display.width(), item_h - 1);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
@@ -590,7 +581,7 @@ public:
snprintf(badge, sizeof(badge), "%d", (int)unread); snprintf(badge, sizeof(badge), "%d", (int)unread);
bw = display.getTextWidth(badge) + 2; bw = display.getTextWidth(badge) + 2;
} }
display.drawTextEllipsized(8, y, display.width() - 10 - bw, ch.name); display.drawTextEllipsized(cw + 2, y, display.width() - cw - 4 - bw, ch.name);
if (unread > 0) { if (unread > 0) {
display.setCursor(display.width() - bw, y); display.setCursor(display.width() - bw, y);
display.print(badge); display.print(badge);
@@ -598,7 +589,8 @@ public:
} }
} }
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
renderScrollHints(display, _channel_scroll, _num_channels); if (_channel_scroll > 0) { display.setCursor(display.width() - cw, start_y); display.print("^"); }
if (_channel_scroll + _visible < _num_channels) { display.setCursor(display.width() - cw, start_y + (_visible-1)*item_h); display.print("v"); }
// Context menu overlay // Context menu overlay
if (_ctx_menu.active) _ctx_menu.render(display); if (_ctx_menu.active) _ctx_menu.render(display);
@@ -623,18 +615,25 @@ public:
return 500; return 500;
} }
int hist_box_h = 2 * lh + 3;
int hist_item_h = hist_box_h + 2;
int hist_start_y = display.headerH();
_hist_visible = (display.height() - hist_start_y) / hist_item_h;
if (_hist_visible < 1) _hist_visible = 1;
int cby = hist_start_y + _hist_visible * hist_item_h + 2;
char title[24]; char title[24];
display.setColor(DisplayDriver::LIGHT); 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.drawTextCentered(display.width()/2, 0, title);
display.fillRect(0, 9, display.width(), 1); display.fillRect(0, lh + 1, display.width(), 1);
int dm_count = dmHistCountForContact(_sel_contact.id.pub_key); int dm_count = dmHistCountForContact(_sel_contact.id.pub_key);
for (int i = 0; i < HIST_VISIBLE && (_dm_hist_scroll + i) < dm_count; i++) { for (int i = 0; i < _hist_visible && (_dm_hist_scroll + i) < dm_count; i++) {
int item = _dm_hist_scroll + i; int item = _dm_hist_scroll + i;
bool sel = (item == _dm_hist_sel); bool sel = (item == _dm_hist_sel);
int y = HIST_START_Y + i * HIST_ITEM_H; int y = hist_start_y + i * hist_item_h;
int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, item); int ring_pos = dmHistEntryForContact(_sel_contact.id.pub_key, item);
if (ring_pos < 0) continue; if (ring_pos < 0) continue;
@@ -647,44 +646,44 @@ public:
if (sel) { if (sel) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y, display.width(), HIST_BOX_H); display.fillRect(0, y, display.width(), hist_box_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.drawTextEllipsized(3, y + 1, display.width() - 6 - age_w, sender); display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender);
if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); } if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); }
display.drawTextEllipsized(3, y + 10, display.width() - 6, skipReplyPrefix(e.text)); display.drawTextEllipsized(3, y + lh + 2, display.width() - cw - 2, skipReplyPrefix(e.text));
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, y, display.width(), HIST_BOX_H); display.drawRect(0, y, display.width(), hist_box_h);
display.fillRect(1, y + 1, display.width() - 2, 8); display.fillRect(1, y + 1, display.width() - 2, lh);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.drawTextEllipsized(3, y + 1, display.width() - 6 - age_w, sender); display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender);
if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); } if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); }
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawTextEllipsized(3, y + 10, display.width() - 6, skipReplyPrefix(e.text)); display.drawTextEllipsized(3, y + lh + 2, display.width() - cw - 2, skipReplyPrefix(e.text));
} }
} }
if (dm_count == 0) { if (dm_count == 0) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawTextCentered(display.width()/2, 32, "No messages yet"); display.drawTextCentered(display.width()/2, display.height()/2, "No messages yet");
} }
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
if (_dm_hist_scroll > 0) { if (_dm_hist_scroll > 0) {
display.setCursor(display.width() - 6, HIST_START_Y + 1); display.setCursor(display.width() - cw, hist_start_y + 1);
display.print("^"); display.print("^");
} }
if (_dm_hist_scroll + HIST_VISIBLE < dm_count) { if (_dm_hist_scroll + _hist_visible < dm_count) {
display.setCursor(display.width() - 6, HIST_START_Y + HIST_VISIBLE * HIST_ITEM_H - 10); display.setCursor(display.width() - cw, hist_start_y + (_hist_visible - 1) * hist_item_h + hist_box_h - lh);
display.print("v"); display.print("v");
} }
bool compose_sel = (_dm_hist_sel == -1); bool compose_sel = (_dm_hist_sel == -1);
const char* ctxt = "[+ send]"; const char* ctxt = "[+ send]";
int ctw = display.getTextWidth(ctxt); int ctw = display.getTextWidth(ctxt);
int cbx = 1, cby = 55; int cbx = 1;
if (compose_sel) { if (compose_sel) {
display.fillRect(cbx, cby - 1, ctw + 4, 9); display.fillRect(cbx, cby - 1, ctw + 4, lh + 1);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} }
display.setCursor(cbx + 2, cby); display.setCursor(cbx + 2, cby);
@@ -718,19 +717,26 @@ public:
return 2000; return 2000;
} }
int hist_box_h = 2 * lh + 3;
int hist_item_h = hist_box_h + 2;
int hist_start_y = display.headerH();
_hist_visible = (display.height() - hist_start_y) / hist_item_h;
if (_hist_visible < 1) _hist_visible = 1;
int cby = hist_start_y + _hist_visible * hist_item_h + 2;
ChannelDetails ch; ChannelDetails ch;
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.drawTextCentered(display.width()/2, 0, title);
display.fillRect(0, 9, display.width(), 1); display.fillRect(0, lh + 1, display.width(), 1);
int ch_hist_count = histCountForChannel(_sel_channel_idx); int ch_hist_count = histCountForChannel(_sel_channel_idx);
for (int i = 0; i < HIST_VISIBLE && (_hist_scroll + i) < ch_hist_count; i++) { for (int i = 0; i < _hist_visible && (_hist_scroll + i) < ch_hist_count; i++) {
int item = _hist_scroll + i; int item = _hist_scroll + i;
bool sel = (item == _hist_sel); bool sel = (item == _hist_sel);
int y = HIST_START_Y + i * HIST_ITEM_H; int y = hist_start_y + i * hist_item_h;
int ring_pos = histEntryForChannel(_sel_channel_idx, item); int ring_pos = histEntryForChannel(_sel_channel_idx, item);
if (ring_pos < 0) continue; if (ring_pos < 0) continue;
@@ -754,36 +760,36 @@ public:
if (sel) { if (sel) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y, display.width(), HIST_BOX_H); display.fillRect(0, y, display.width(), hist_box_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.drawTextEllipsized(3, y + 1, display.width() - 6 - age_w, sender); display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender);
if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); } if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); }
display.drawTextEllipsized(3, y + 10, display.width() - 6, skipReplyPrefix(msg_part)); display.drawTextEllipsized(3, y + lh + 2, display.width() - cw - 2, skipReplyPrefix(msg_part));
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, y, display.width(), HIST_BOX_H); display.drawRect(0, y, display.width(), hist_box_h);
display.fillRect(1, y + 1, display.width() - 2, 8); display.fillRect(1, y + 1, display.width() - 2, lh);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.drawTextEllipsized(3, y + 1, display.width() - 6 - age_w, sender); display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender);
if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); } if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); }
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawTextEllipsized(3, y + 10, display.width() - 6, skipReplyPrefix(msg_part)); display.drawTextEllipsized(3, y + lh + 2, display.width() - cw - 2, skipReplyPrefix(msg_part));
} }
} }
if (ch_hist_count == 0) { if (ch_hist_count == 0) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawTextCentered(display.width()/2, 32, "No messages yet"); display.drawTextCentered(display.width()/2, display.height()/2, "No messages yet");
} }
// scroll hints // scroll hints
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
if (_hist_scroll > 0) { if (_hist_scroll > 0) {
display.setCursor(display.width() - 6, HIST_START_Y + 1); display.setCursor(display.width() - cw, hist_start_y + 1);
display.print("^"); display.print("^");
} }
if (_hist_scroll + HIST_VISIBLE < ch_hist_count) { if (_hist_scroll + _hist_visible < ch_hist_count) {
display.setCursor(display.width() - 6, HIST_START_Y + HIST_VISIBLE * HIST_ITEM_H - 10); display.setCursor(display.width() - cw, hist_start_y + (_hist_visible - 1) * hist_item_h + hist_box_h - lh);
display.print("v"); display.print("v");
} }
@@ -791,14 +797,14 @@ public:
bool compose_sel = (_hist_sel == -1); bool compose_sel = (_hist_sel == -1);
const char* ctxt = "[+ send]"; const char* ctxt = "[+ send]";
int ctw = display.getTextWidth(ctxt); int ctw = display.getTextWidth(ctxt);
int cbx = 1, cby = 55; int cbx = 1;
if (compose_sel) { if (compose_sel) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(cbx - 1, cby - 1, ctw + 4, 10); display.fillRect(cbx - 1, cby - 1, ctw + 4, lh + 2);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawRect(cbx - 1, cby - 1, ctw + 4, 10); display.drawRect(cbx - 1, cby - 1, ctw + 4, lh + 2);
} }
display.setCursor(cbx + 1, cby); display.setCursor(cbx + 1, cby);
display.print(ctxt); display.print(ctxt);
@@ -826,17 +832,17 @@ public:
snprintf(title, sizeof(title), "TO:%.14s", _sel_contact.name); snprintf(title, sizeof(title), "TO:%.14s", _sel_contact.name);
} }
display.drawTextCentered(display.width()/2, 0, title); display.drawTextCentered(display.width()/2, 0, title);
display.fillRect(0, 10, display.width(), 1); display.fillRect(0, display.headerH() - 1, display.width(), 1);
int total_msg_items = 1 + _active_msg_count; int total_msg_items = 1 + _active_msg_count;
for (int i = 0; i < VISIBLE && (_msg_scroll+i) < total_msg_items; i++) { for (int i = 0; i < _visible && (_msg_scroll+i) < total_msg_items; i++) {
int idx = _msg_scroll + i; int idx = _msg_scroll + i;
bool sel = (idx == _msg_sel); bool sel = (idx == _msg_sel);
int y = START_Y + i * ITEM_H; int y = start_y + i * item_h;
if (sel) { if (sel) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y - 1, display.width(), ITEM_H - 1); display.fillRect(0, y - 1, display.width(), item_h - 1);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
@@ -845,17 +851,18 @@ public:
display.print(sel ? ">" : " "); display.print(sel ? ">" : " ");
if (idx == 0) { if (idx == 0) {
display.setCursor(8, y); display.setCursor(cw + 2, y);
display.print("Custom message..."); display.print("Custom message...");
} else { } else {
NodePrefs* p = _task->getNodePrefs(); NodePrefs* p = _task->getNodePrefs();
int slot = _active_msgs[idx - 1]; int slot = _active_msgs[idx - 1];
const char* tmpl = p ? p->custom_msgs[slot] : ""; const char* tmpl = p ? p->custom_msgs[slot] : "";
display.drawTextEllipsized(8, y, display.width() - 10, tmpl); display.drawTextEllipsized(cw + 2, y, display.width() - cw - 4, tmpl);
} }
} }
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
renderScrollHints(display, _msg_scroll, total_msg_items); if (_msg_scroll > 0) { display.setCursor(display.width() - cw, start_y); display.print("^"); }
if (_msg_scroll + _visible < total_msg_items) { display.setCursor(display.width() - cw, start_y + (_visible-1)*item_h); display.print("v"); }
} }
return 2000; return 2000;
} }
@@ -911,7 +918,7 @@ public:
} }
if (c == KEY_DOWN && _contact_sel < _num_contacts - 1) { if (c == KEY_DOWN && _contact_sel < _num_contacts - 1) {
_contact_sel++; _contact_sel++;
if (_contact_sel >= _contact_scroll + VISIBLE) _contact_scroll = _contact_sel - VISIBLE + 1; if (_contact_sel >= _contact_scroll + _visible) _contact_scroll = _contact_sel - _visible + 1;
return true; return true;
} }
if (c == KEY_ENTER && _num_contacts > 0) { if (c == KEY_ENTER && _num_contacts > 0) {
@@ -972,7 +979,7 @@ public:
} }
if (c == KEY_DOWN && _channel_sel < _num_channels - 1) { if (c == KEY_DOWN && _channel_sel < _num_channels - 1) {
_channel_sel++; _channel_sel++;
if (_channel_sel >= _channel_scroll + VISIBLE) _channel_scroll = _channel_sel - VISIBLE + 1; if (_channel_sel >= _channel_scroll + _visible) _channel_scroll = _channel_sel - _visible + 1;
return true; return true;
} }
if (c == KEY_ENTER && _num_channels > 0) { if (c == KEY_ENTER && _num_channels > 0) {
@@ -1055,8 +1062,8 @@ public:
if (_dm_hist_sel == -1 && dm_count > 0) { _dm_hist_sel = 0; _dm_hist_scroll = 0; } if (_dm_hist_sel == -1 && dm_count > 0) { _dm_hist_sel = 0; _dm_hist_scroll = 0; }
else if (_dm_hist_sel >= 0 && _dm_hist_sel < dm_count - 1) { else if (_dm_hist_sel >= 0 && _dm_hist_sel < dm_count - 1) {
_dm_hist_sel++; _dm_hist_sel++;
if (_dm_hist_sel >= _dm_hist_scroll + HIST_VISIBLE) if (_dm_hist_sel >= _dm_hist_scroll + _hist_visible)
_dm_hist_scroll = _dm_hist_sel - HIST_VISIBLE + 1; _dm_hist_scroll = _dm_hist_sel - _hist_visible + 1;
} }
return true; return true;
} }
@@ -1129,7 +1136,7 @@ public:
if (_hist_sel == -1 && ch_hist_count > 0) { _hist_sel = 0; _hist_scroll = 0; } if (_hist_sel == -1 && ch_hist_count > 0) { _hist_sel = 0; _hist_scroll = 0; }
else if (_hist_sel >= 0 && _hist_sel < ch_hist_count - 1) { else if (_hist_sel >= 0 && _hist_sel < ch_hist_count - 1) {
_hist_sel++; _hist_sel++;
if (_hist_sel >= _hist_scroll + HIST_VISIBLE) _hist_scroll = _hist_sel - HIST_VISIBLE + 1; if (_hist_sel >= _hist_scroll + _hist_visible) _hist_scroll = _hist_sel - _hist_visible + 1;
} }
updateChannelUnread(); updateChannelUnread();
return true; return true;
@@ -1183,7 +1190,7 @@ public:
} }
if (c == KEY_DOWN && _msg_sel < total_msg_items - 1) { if (c == KEY_DOWN && _msg_sel < total_msg_items - 1) {
_msg_sel++; _msg_sel++;
if (_msg_sel >= _msg_scroll + VISIBLE) _msg_scroll = _msg_sel - VISIBLE + 1; if (_msg_sel >= _msg_scroll + _visible) _msg_scroll = _msg_sel - _visible + 1;
return true; return true;
} }
if (c == KEY_ENTER) { if (c == KEY_ENTER) {
@@ -6,12 +6,11 @@ class RingtoneEditorScreen : public UIScreen {
UITask* _task; UITask* _task;
NodePrefs* _prefs; NodePrefs* _prefs;
static const int MAX_NOTES = 32; static const int MAX_NOTES = 32;
static const int VISIBLE_NOTES = 7; static const int CELL_W = 18;
static const int CELL_W = 18; static const int MENU_VISIBLE = 5;
static const int NOTES_Y = 12;
static const int CELL_H = 14; int _visible_notes = 7; // updated in render(); used by clampScroll()
static const int MENU_VISIBLE = 5;
static const uint16_t BPM_OPTS[5]; static const uint16_t BPM_OPTS[5];
static const uint8_t DUR_VALS[4]; static const uint8_t DUR_VALS[4];
@@ -43,8 +42,8 @@ class RingtoneEditorScreen : public UIScreen {
} }
void clampScroll() { void clampScroll() {
if (_cursor < _scroll) _scroll = _cursor; if (_cursor < _scroll) _scroll = _cursor;
if (_cursor >= _scroll + VISIBLE_NOTES) _scroll = _cursor - VISIBLE_NOTES + 1; if (_cursor >= _scroll + _visible_notes) _scroll = _cursor - _visible_notes + 1;
if (_scroll < 0) _scroll = 0; if (_scroll < 0) _scroll = 0;
} }
@@ -93,13 +92,18 @@ public:
display.setTextSize(1); display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
const int lh = display.getLineHeight();
const int notes_y = display.listStart();
const int cell_h = lh + 6;
_visible_notes = display.width() / CELL_W;
char hdr[32]; char hdr[32];
snprintf(hdr, sizeof(hdr), "M%d BPM:%u %d/%d", _slot + 1, BPM_OPTS[_bpm_idx], _len, MAX_NOTES); snprintf(hdr, sizeof(hdr), "M%d BPM:%u %d/%d", _slot + 1, BPM_OPTS[_bpm_idx], _len, MAX_NOTES);
display.setCursor(0, 0); display.setCursor(0, 0);
display.print(hdr); display.print(hdr);
display.fillRect(0, 10, display.width(), 1); display.fillRect(0, display.headerH() - 1, display.width(), 1);
for (int i = 0; i < VISIBLE_NOTES; i++) { for (int i = 0; i < _visible_notes; i++) {
int ni = _scroll + i; int ni = _scroll + i;
int cx = i * CELL_W; int cx = i * CELL_W;
bool sel = (ni == _cursor); bool sel = (ni == _cursor);
@@ -116,50 +120,52 @@ public:
} }
if (sel) { if (sel) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(cx, NOTES_Y, CELL_W - 1, CELL_H); display.fillRect(cx, notes_y, CELL_W - 1, cell_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawRect(cx, NOTES_Y, CELL_W - 1, CELL_H); display.drawRect(cx, notes_y, CELL_W - 1, cell_h);
} }
display.setCursor(cx + 3, NOTES_Y + 3); display.setCursor(cx + 3, notes_y + 3);
display.print(label); display.print(label);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
} else if (ni == _len && _len < MAX_NOTES) { } else if (ni == _len && _len < MAX_NOTES) {
if (sel) { if (sel) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(cx, NOTES_Y, CELL_W - 1, CELL_H); display.fillRect(cx, notes_y, CELL_W - 1, cell_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
} }
display.setCursor(cx + 6, NOTES_Y + 3); display.setCursor(cx + 6, notes_y + 3);
display.print("+"); display.print("+");
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
} }
} }
int info_y = NOTES_Y + CELL_H + 2; const int info_y = notes_y + cell_h + 2;
const int bottom_y = display.height() - display.lineStep();
if (_scroll > 0) { display.setCursor(0, info_y); display.print("<"); } if (_scroll > 0) { display.setCursor(0, info_y); display.print("<"); }
if (_scroll + VISIBLE_NOTES <= _len) { display.setCursor(display.width() - 6, info_y); display.print(">"); } if (_scroll + _visible_notes <= _len) { display.setCursor(display.width() - display.getCharWidth(), info_y); display.print(">"); }
if (_cursor < _len) { if (_cursor < _len) {
char info[24]; char info[24];
snprintf(info, sizeof(info), "oct:%d dur:%s", snprintf(info, sizeof(info), "oct:%d dur:%s",
noteOctave(_notes[_cursor]), DUR_LABELS[noteDurIdx(_notes[_cursor])]); noteOctave(_notes[_cursor]), DUR_LABELS[noteDurIdx(_notes[_cursor])]);
display.setCursor(10, info_y); display.setCursor(display.getCharWidth() + 2, info_y);
display.print(info); display.print(info);
} else if (_cursor == _len) { } else if (_cursor == _len) {
display.setCursor(10, info_y); display.setCursor(display.getCharWidth() + 2, info_y);
display.print("U/D to add note"); display.print("U/D to add note");
} }
display.setCursor(0, 54); display.setCursor(0, bottom_y);
display.print("ENT:oct MENU:opts"); display.print("ENT:oct MENU:opts");
if (_menu_open) { if (_menu_open) {
static const int mw = 100, mh = MENU_VISIBLE * 10 + 4; const int menu_ih = display.lineStep();
int mx = (128 - mw) / 2; const int mw = 100, mh = MENU_VISIBLE * menu_ih + 4;
int my = (64 - mh) / 2; int mx = (display.width() - mw) / 2;
int my = (display.height() - mh) / 2;
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.fillRect(mx, my, mw, mh); display.fillRect(mx, my, mw, mh);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
@@ -167,10 +173,10 @@ public:
for (int i = 0; i < MENU_VISIBLE; i++) { for (int i = 0; i < MENU_VISIBLE; i++) {
int item = _menu_scroll + i; int item = _menu_scroll + i;
if (item >= M_COUNT) break; if (item >= M_COUNT) break;
int iy = my + 2 + i * 10; int iy = my + 2 + i * menu_ih;
bool sel = (item == _menu_sel); bool sel = (item == _menu_sel);
if (sel) { if (sel) {
display.fillRect(mx + 1, iy - 1, mw - 2, 10); display.fillRect(mx + 1, iy - 1, mw - 2, menu_ih);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} }
display.setCursor(mx + 4, iy); display.setCursor(mx + 4, iy);
@@ -182,8 +188,9 @@ public:
display.print(MENU_LABELS[item]); display.print(MENU_LABELS[item]);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
} }
if (_menu_scroll > 0) { display.setCursor(mx + mw - 7, my + 2); display.print("^"); } int cw = display.getCharWidth();
if (_menu_scroll + MENU_VISIBLE < M_COUNT) { display.setCursor(mx + mw - 7, my + mh - 10); display.print("v"); } if (_menu_scroll > 0) { display.setCursor(mx + mw - cw - 1, my + 2); display.print("^"); }
if (_menu_scroll + MENU_VISIBLE < M_COUNT) { display.setCursor(mx + mw - cw - 1, my + mh - menu_ih); display.print("v"); }
} }
return 200; return 200;
@@ -57,13 +57,9 @@ class SettingsScreen : public UIScreen {
Count Count
}; };
static const int VISIBLE_ITEMS = 4;
static const int ITEM_H = 11;
static const int START_Y = 12;
static const int VAL_X = 80;
int _selected; int _selected;
int _scroll; int _scroll;
int _visible; // items fitting on screen; updated each render, used by handleInput
bool _dirty; bool _dirty;
static const uint16_t AUTO_OFF_OPTS[5]; static const uint16_t AUTO_OFF_OPTS[5];
@@ -230,7 +226,7 @@ class SettingsScreen : public UIScreen {
if (sel) { if (sel) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y - 1, display.width(), ITEM_H); display.fillRect(0, y - 1, display.width(), display.lineStep());
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
@@ -242,10 +238,10 @@ class SettingsScreen : public UIScreen {
if (item == BRIGHTNESS) { if (item == BRIGHTNESS) {
display.print("Bright"); display.print("Bright");
renderBar(display, VAL_X, y, (p ? p->display_brightness : 2) + 1, 5); renderBar(display, display.valCol(), y, (p ? p->display_brightness : 2) + 1, 5);
} else if (item == BUZZER) { } else if (item == BUZZER) {
display.print("Buzzer"); display.print("Buzzer");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
#ifdef PIN_BUZZER #ifdef PIN_BUZZER
{ static const char* labels[] = { "ON", "OFF", "Auto" }; { static const char* labels[] = { "ON", "OFF", "Auto" };
int m = _task->getBuzzerMode(); int m = _task->getBuzzerMode();
@@ -256,45 +252,45 @@ class SettingsScreen : public UIScreen {
} else if (item == BUZZER_VOLUME) { } else if (item == BUZZER_VOLUME) {
display.print("BzrVol"); display.print("BzrVol");
#ifdef PIN_BUZZER #ifdef PIN_BUZZER
renderBar(display, VAL_X, y, _task->getBuzzerVolume() + 1, 5); renderBar(display, display.valCol(), y, _task->getBuzzerVolume() + 1, 5);
#else #else
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
display.print("N/A"); display.print("N/A");
#endif #endif
} else if (item == DM_MELODY) { } else if (item == DM_MELODY) {
display.print("DM sound"); display.print("DM sound");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
{ static const char* L[] = { "built-in", "M1", "M2" }; { static const char* L[] = { "built-in", "M1", "M2" };
uint8_t v = p ? p->notif_melody_dm : 0; uint8_t v = p ? p->notif_melody_dm : 0;
display.print(L[v < 3 ? v : 0]); } display.print(L[v < 3 ? v : 0]); }
} else if (item == CH_MELODY) { } else if (item == CH_MELODY) {
display.print("Ch sound"); display.print("Ch sound");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
{ static const char* L[] = { "built-in", "M1", "M2" }; { static const char* L[] = { "built-in", "M1", "M2" };
uint8_t v = p ? p->notif_melody_ch : 0; uint8_t v = p ? p->notif_melody_ch : 0;
display.print(L[v < 3 ? v : 0]); } display.print(L[v < 3 ? v : 0]); }
} else if (isHomePage(item)) { } else if (isHomePage(item)) {
display.print(homePageLabel(item)); display.print(homePageLabel(item));
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
display.print(homePageVisible(item, p) ? "ON" : "OFF"); display.print(homePageVisible(item, p) ? "ON" : "OFF");
} else if (item == TX_POWER) { } else if (item == TX_POWER) {
display.print("TX Pwr"); display.print("TX Pwr");
char buf[8]; char buf[8];
sprintf(buf, "%ddBm", p ? p->tx_power_dbm : 0); sprintf(buf, "%ddBm", p ? p->tx_power_dbm : 0);
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
display.print(buf); display.print(buf);
} else if (item == AUTO_OFF) { } else if (item == AUTO_OFF) {
display.print("AutoOff"); display.print("AutoOff");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
display.print(AUTO_OFF_LABELS[autoOffIndex()]); display.print(AUTO_OFF_LABELS[autoOffIndex()]);
} else if (item == AUTO_LOCK) { } else if (item == AUTO_LOCK) {
display.print("AutoLock"); display.print("AutoLock");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
display.print((p && p->auto_lock) ? "ON" : "OFF"); display.print((p && p->auto_lock) ? "ON" : "OFF");
#if ENV_INCLUDE_GPS == 1 #if ENV_INCLUDE_GPS == 1
} else if (item == GPS_INTERVAL) { } else if (item == GPS_INTERVAL) {
display.print("GPS upd"); display.print("GPS upd");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
display.print(GPS_INTERVAL_LABELS[gpsIntervalIndex()]); display.print(GPS_INTERVAL_LABELS[gpsIntervalIndex()]);
#endif #endif
} else if (item == TIMEZONE) { } else if (item == TIMEZONE) {
@@ -303,46 +299,46 @@ class SettingsScreen : public UIScreen {
int8_t tz = p ? p->tz_offset_hours : 0; int8_t tz = p ? p->tz_offset_hours : 0;
if (tz >= 0) sprintf(buf, "UTC+%d", (int)tz); if (tz >= 0) sprintf(buf, "UTC+%d", (int)tz);
else sprintf(buf, "UTC%d", (int)tz); else sprintf(buf, "UTC%d", (int)tz);
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
display.print(buf); display.print(buf);
} else if (item == LOW_BAT) { } else if (item == LOW_BAT) {
display.print("LowBat"); display.print("LowBat");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
display.print(LOW_BAT_LABELS[lowBatIndex()]); display.print(LOW_BAT_LABELS[lowBatIndex()]);
} else if (item == BATT_DISPLAY) { } else if (item == BATT_DISPLAY) {
display.print("BattDisp"); display.print("BattDisp");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
uint8_t mode = p ? p->batt_display_mode : 0; uint8_t mode = p ? p->batt_display_mode : 0;
display.print(BATT_DISPLAY_LABELS[mode < BATT_DISPLAY_COUNT ? mode : 0]); display.print(BATT_DISPLAY_LABELS[mode < BATT_DISPLAY_COUNT ? mode : 0]);
#if !defined(EINK_DISPLAY_MODEL) #if !defined(EINK_DISPLAY_MODEL)
} else if (item == CLOCK_SECONDS) { } else if (item == CLOCK_SECONDS) {
display.print("Seconds"); display.print("Seconds");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
display.print((p && p->clock_hide_seconds) ? "OFF" : "ON"); display.print((p && p->clock_hide_seconds) ? "OFF" : "ON");
#endif #endif
} else if (item == CLOCK_FORMAT) { } else if (item == CLOCK_FORMAT) {
display.print("Format"); display.print("Format");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
display.print((p && p->clock_12h) ? "12h" : "24h"); display.print((p && p->clock_12h) ? "12h" : "24h");
} else if (item == FONT) { } else if (item == FONT) {
display.print("Font"); display.print("Font");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
display.print((p && p->use_lemon_font) ? "Lemon" : "Default"); display.print((p && p->use_lemon_font) ? "Lemon" : "Default");
#if defined(EINK_DISPLAY_MODEL) #if defined(EINK_DISPLAY_MODEL)
} else if (item == ROTATION) { } else if (item == ROTATION) {
display.print("Rotation"); display.print("Rotation");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
{ static const char* ROT_LABELS[] = { "0deg", "90deg", "180deg", "270deg" }; { static const char* ROT_LABELS[] = { "0deg", "90deg", "180deg", "270deg" };
uint8_t r = p ? (p->display_rotation & 3) : 0; uint8_t r = p ? (p->display_rotation & 3) : 0;
display.print(ROT_LABELS[r]); } display.print(ROT_LABELS[r]); }
#endif #endif
} else if (item == DM_FILTER) { } else if (item == DM_FILTER) {
display.print("DM"); display.print("DM");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
display.print((p && p->dm_show_all) ? "all" : "fav"); display.print((p && p->dm_show_all) ? "all" : "fav");
} else if (item == ROOM_FILTER) { } else if (item == ROOM_FILTER) {
display.print("Rooms"); display.print("Rooms");
display.setCursor(VAL_X, y); display.setCursor(display.valCol(), y);
display.print((p && p->room_fav_only) ? "fav" : "all"); display.print((p && p->room_fav_only) ? "fav" : "all");
} else if (isMsgSlot(item)) { } else if (isMsgSlot(item)) {
int slot = msgSlotIndex(item); int slot = msgSlotIndex(item);
@@ -350,7 +346,8 @@ class SettingsScreen : public UIScreen {
snprintf(label, sizeof(label), "Q%d:", slot + 1); snprintf(label, sizeof(label), "Q%d:", slot + 1);
display.print(label); display.print(label);
const char* tmpl = (p && p->custom_msgs[slot][0]) ? p->custom_msgs[slot] : "(empty)"; const char* tmpl = (p && p->custom_msgs[slot][0]) ? p->custom_msgs[slot] : "(empty)";
display.drawTextEllipsized(VAL_X - 20, y, display.width() - VAL_X + 18, tmpl); int xm = 8 + display.getCharWidth() * 4;
display.drawTextEllipsized(xm, y, display.width() - xm, tmpl);
} }
} }
@@ -360,7 +357,7 @@ class SettingsScreen : public UIScreen {
public: public:
SettingsScreen(UITask* task) SettingsScreen(UITask* task)
: _task(task), _selected(BRIGHTNESS), _scroll(0), _dirty(false), _edit_slot(-1) {} : _task(task), _selected(BRIGHTNESS), _scroll(0), _visible(4), _dirty(false), _edit_slot(-1) {}
void markClean() { _dirty = false; } void markClean() { _dirty = false; }
@@ -372,23 +369,27 @@ public:
return _kb.render(display); return _kb.render(display);
} }
int item_h = display.lineStep();
int start_y = display.listStart();
_visible = display.listVisible(item_h);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawTextCentered(display.width() / 2, 0, "SETTINGS"); display.drawTextCentered(display.width() / 2, 0, "SETTINGS");
display.fillRect(0, 10, display.width(), 1); display.fillRect(0, display.headerH() - 2, display.width(), 1);
for (int i = 0; i < VISIBLE_ITEMS && (_scroll + i) < SettingItem::Count; i++) { for (int i = 0; i < _visible && (_scroll + i) < SettingItem::Count; i++) {
renderItem(display, _scroll + i, START_Y + i * ITEM_H); renderItem(display, _scroll + i, start_y + i * item_h);
} }
// scroll indicators // scroll indicators
if (_scroll > 0) { if (_scroll > 0) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.setCursor(display.width() - 6, START_Y); display.setCursor(display.width() - display.getCharWidth(), start_y);
display.print("^"); display.print("^");
} }
if (_scroll + VISIBLE_ITEMS < SettingItem::Count) { if (_scroll + _visible < SettingItem::Count) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.setCursor(display.width() - 6, START_Y + (VISIBLE_ITEMS - 1) * ITEM_H); display.setCursor(display.width() - display.getCharWidth(), start_y + (_visible - 1) * item_h);
display.print("v"); display.print("v");
} }
@@ -426,7 +427,7 @@ public:
int next = nextSelectable(_selected); int next = nextSelectable(_selected);
if (next != _selected) { if (next != _selected) {
_selected = next; _selected = next;
if (_selected >= _scroll + VISIBLE_ITEMS) _scroll = _selected - VISIBLE_ITEMS + 1; if (_selected >= _scroll + _visible) _scroll = _selected - _visible + 1;
} }
return true; return true;
} }
+84 -59
View File
@@ -65,27 +65,36 @@ public:
} }
int render(DisplayDriver& display) override { int render(DisplayDriver& display) override {
display.setTextSize(1);
const int lh = display.getLineHeight();
const int step = display.lineStep();
// meshcore logo // meshcore logo
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
int logoWidth = 128; int logoWidth = 128;
display.drawXbm((display.width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); int logo_y = 3;
display.drawXbm((display.width() - logoWidth) / 2, logo_y, meshcore_logo, logoWidth, 13);
// version info // version info at sz2
int ver_y = logo_y + 13 + 2;
display.setTextSize(2); display.setTextSize(2);
display.drawTextCentered(display.width()/2, 22, _version_info); display.drawTextCentered(display.width()/2, ver_y, _version_info);
// build date at sz1 — sz2 lh is always 16
int date_y = ver_y + 16 + 2;
display.setTextSize(1); display.setTextSize(1);
display.drawTextCentered(display.width()/2, 42, FIRMWARE_BUILD_DATE); display.drawTextCentered(display.width()/2, date_y, FIRMWARE_BUILD_DATE);
#ifdef FIRMWARE_PLUS_BUILD #ifdef FIRMWARE_PLUS_BUILD
display.fillRect(0, 53, display.width(), 10); int plus_y = date_y + step;
display.fillRect(0, plus_y - 1, display.width(), lh + 2);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
char plus_label[24]; char plus_label[24];
if (_plus_ver[0]) if (_plus_ver[0])
snprintf(plus_label, sizeof(plus_label), "Plus %s for Wio", _plus_ver); snprintf(plus_label, sizeof(plus_label), "Plus %s for Wio", _plus_ver);
else else
snprintf(plus_label, sizeof(plus_label), "Plus for Wio"); snprintf(plus_label, sizeof(plus_label), "Plus for Wio");
display.drawTextCentered(display.width()/2, 54, plus_label); display.drawTextCentered(display.width()/2, plus_y, plus_label);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
#endif #endif
@@ -338,9 +347,14 @@ public:
int render(DisplayDriver& display) override { int render(DisplayDriver& display) override {
char tmp[80]; char tmp[80];
display.setTextSize(1);
const int lh = display.getLineHeight(); // line height at sz1
const int step = display.lineStep(); // lh + 2
const int dots_y = lh + 4; // page-dot row: just below header
const int content_y = dots_y + step; // first content row
// node name + battery — hidden on CLOCK page (full screen used for dashboard) // node name + battery — hidden on CLOCK page (full screen used for dashboard)
if (_page != CLOCK) { if (_page != CLOCK) {
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
char filtered_name[sizeof(_node_prefs->node_name)]; char filtered_name[sizeof(_node_prefs->node_name)];
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name)); display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
@@ -360,13 +374,12 @@ public:
if (i == _page) curr_vis = vis_count; if (i == _page) curr_vis = vis_count;
vis_count++; vis_count++;
} }
int y = 14;
int x = display.width() / 2 - 5 * (vis_count - 1); int x = display.width() / 2 - 5 * (vis_count - 1);
int vi = 0; int vi = 0;
for (int i = 0; i < (int)Count; i++) { for (int i = 0; i < (int)Count; i++) {
if (!isPageVisible(i)) continue; if (!isPageVisible(i)) continue;
if (vi == curr_vis) display.fillRect(x-1, y-1, 3, 3); if (vi == curr_vis) display.fillRect(x-1, dots_y-1, 3, 3);
else display.fillRect(x, y, 1, 1); else display.fillRect(x, dots_y, 1, 1);
x += 10; vi++; x += 10; vi++;
} }
} }
@@ -376,9 +389,10 @@ public:
if (unix_ts < 1000000000UL) { if (unix_ts < 1000000000UL) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.setTextSize(1); display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 25, "! No time sync"); int mid_y = display.height() / 2 - step;
display.drawTextCentered(display.width() / 2, 40, "Enable GPS or"); display.drawTextCentered(display.width() / 2, mid_y, "! No time sync");
display.drawTextCentered(display.width() / 2, 51, "connect app"); display.drawTextCentered(display.width() / 2, mid_y + step, "Enable GPS or");
display.drawTextCentered(display.width() / 2, mid_y + step * 2, "connect app");
} else { } else {
int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0; int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0;
unix_ts += (int32_t)tz * 3600; unix_ts += (int32_t)tz * 3600;
@@ -407,12 +421,14 @@ public:
sprintf(buf, "%s %d %s %d", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon], 1900 + ti->tm_year); sprintf(buf, "%s %d %s %d", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon], 1900 + ti->tm_year);
display.drawTextCentered(display.width() / 2, 19, buf); display.drawTextCentered(display.width() / 2, 19, buf);
display.fillRect(0, 28, display.width(), 1); int sep_y = 19 + lh + 1;
int dash0 = sep_y + 3;
display.fillRect(0, sep_y, display.width(), 1);
// dashboard data fields // dashboard data fields
if (_node_prefs) { if (_node_prefs) {
refresh_sensors(); refresh_sensors();
static const int FIELD_Y[] = { 31, 41, 51 }; const int FIELD_Y[3] = { dash0, dash0 + step, dash0 + step * 2 };
for (int fi = 0; fi < 3; fi++) { for (int fi = 0; fi < 3; fi++) {
uint8_t field = _node_prefs->dashboard_fields[fi]; uint8_t field = _node_prefs->dashboard_fields[fi];
if (field == DASH_NONE) continue; if (field == DASH_NONE) continue;
@@ -491,8 +507,8 @@ public:
} else if (_page == HomePage::RECENT) { } else if (_page == HomePage::RECENT) {
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
int y = 20; int y = content_y;
for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) { for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += step) {
auto a = &recent[i]; auto a = &recent[i];
if (a->name[0] == 0) continue; // empty slot if (a->name[0] == 0) continue; // empty slot
int secs = _rtc->getCurrentTime() - a->recv_timestamp; int secs = _rtc->getCurrentTime() - a->recv_timestamp;
@@ -515,44 +531,46 @@ public:
} }
} else if (_page == HomePage::RADIO) { } else if (_page == HomePage::RADIO) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.setTextSize(1);
// freq / sf // freq / sf
display.setCursor(0, 20); display.setCursor(0, content_y);
sprintf(tmp, "FQ: %06.3f SF: %d", _node_prefs->freq, _node_prefs->sf); sprintf(tmp, "FQ: %06.3f SF: %d", _node_prefs->freq, _node_prefs->sf);
display.print(tmp); display.print(tmp);
display.setCursor(0, 31); display.setCursor(0, content_y + step);
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr); sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
display.print(tmp); display.print(tmp);
// tx power, noise floor // tx power, noise floor
display.setCursor(0, 42); display.setCursor(0, content_y + step * 2);
sprintf(tmp, "TX: %ddBm", _node_prefs->tx_power_dbm); sprintf(tmp, "TX: %ddBm", _node_prefs->tx_power_dbm);
display.print(tmp); display.print(tmp);
display.setCursor(0, 53); display.setCursor(0, content_y + step * 3);
sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor()); sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor());
display.print(tmp); display.print(tmp);
} else if (_page == HomePage::BLUETOOTH) { } else if (_page == HomePage::BLUETOOTH) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawXbm((display.width() - 32) / 2, 14, int icon_y = content_y;
display.drawXbm((display.width() - 32) / 2, icon_y,
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off, _task->isSerialEnabled() ? bluetooth_on : bluetooth_off,
32, 32); 32, 32);
display.setTextSize(1); display.setTextSize(1);
int pin_y = icon_y + 32 + 3;
int hint_y = pin_y + step;
if (_task->isSerialEnabled() && !_task->hasConnection() && the_mesh.getBLEPin() != 0) { if (_task->isSerialEnabled() && !_task->hasConnection() && the_mesh.getBLEPin() != 0) {
char pin_buf[16]; char pin_buf[16];
snprintf(pin_buf, sizeof(pin_buf), "PIN: %d", the_mesh.getBLEPin()); snprintf(pin_buf, sizeof(pin_buf), "PIN: %d", the_mesh.getBLEPin());
display.drawTextCentered(display.width() / 2, 49, pin_buf); display.drawTextCentered(display.width() / 2, pin_y, pin_buf);
} }
display.drawTextCentered(display.width() / 2, 57, "toggle: " PRESS_LABEL); display.drawTextCentered(display.width() / 2, hint_y, "toggle: " PRESS_LABEL);
} else if (_page == HomePage::ADVERT) { } else if (_page == HomePage::ADVERT) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32); display.drawXbm((display.width() - 32) / 2, content_y, advert_icon, 32, 32);
display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL); display.drawTextCentered(display.width() / 2, content_y + 32 + 3, "advert: " PRESS_LABEL);
#if ENV_INCLUDE_GPS == 1 #if ENV_INCLUDE_GPS == 1
} else if (_page == HomePage::GPS) { } else if (_page == HomePage::GPS) {
LocationProvider* nmea = sensors.getLocationProvider(); LocationProvider* nmea = sensors.getLocationProvider();
char buf[50]; char buf[50];
int y = 18; int y = content_y;
bool gps_state = _task->getGPSState(); bool gps_state = _task->getGPSState();
#ifdef PIN_GPS_SWITCH #ifdef PIN_GPS_SWITCH
bool hw_gps_state = digitalRead(PIN_GPS_SWITCH); bool hw_gps_state = digitalRead(PIN_GPS_SWITCH);
@@ -566,30 +584,30 @@ public:
#endif #endif
display.drawTextLeftAlign(0, y, buf); display.drawTextLeftAlign(0, y, buf);
if (nmea == NULL) { if (nmea == NULL) {
y = y + 12; y += step;
display.drawTextLeftAlign(0, y, "Can't access GPS"); display.drawTextLeftAlign(0, y, "Can't access GPS");
} else { } else {
strcpy(buf, nmea->isValid()?"fix":"no fix"); strcpy(buf, nmea->isValid()?"fix":"no fix");
display.drawTextRightAlign(display.width()-1, y, buf); display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12; y += step;
display.drawTextLeftAlign(0, y, "sat"); display.drawTextLeftAlign(0, y, "sat");
sprintf(buf, "%d", nmea->satellitesCount()); sprintf(buf, "%d", nmea->satellitesCount());
display.drawTextRightAlign(display.width()-1, y, buf); display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12; y += step;
display.drawTextLeftAlign(0, y, "pos"); display.drawTextLeftAlign(0, y, "pos");
sprintf(buf, "%.4f %.4f", sprintf(buf, "%.4f %.4f",
nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.); nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.);
display.drawTextRightAlign(display.width()-1, y, buf); display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12; y += step;
display.drawTextLeftAlign(0, y, "alt"); display.drawTextLeftAlign(0, y, "alt");
sprintf(buf, "%.2f", nmea->getAltitude()/1000.); sprintf(buf, "%.2f", nmea->getAltitude()/1000.);
display.drawTextRightAlign(display.width()-1, y, buf); display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12; y += step;
} }
#endif #endif
#if UI_SENSORS_PAGE == 1 #if UI_SENSORS_PAGE == 1
} else if (_page == HomePage::SENSORS) { } else if (_page == HomePage::SENSORS) {
int y = 18; int y = content_y;
refresh_sensors(); refresh_sensors();
uint8_t avail_types[16]; uint8_t avail_types[16];
@@ -655,7 +673,7 @@ public:
display.print(name); display.print(name);
display.setCursor(display.width() - display.getTextWidth(buf) - 1, y); display.setCursor(display.width() - display.getTextWidth(buf) - 1, y);
display.print(buf); display.print(buf);
y += 12; y += step;
} }
if (need_scroll) sensors_scroll_offset = (sensors_scroll_offset + 1) % avail_count; if (need_scroll) sensors_scroll_offset = (sensors_scroll_offset + 1) % avail_count;
else sensors_scroll_offset = 0; else sensors_scroll_offset = 0;
@@ -663,32 +681,32 @@ public:
} else if (_page == HomePage::SETTINGS) { } else if (_page == HomePage::SETTINGS) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.setTextSize(1); display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 22, "Settings"); display.drawTextCentered(display.width() / 2, content_y, "Settings");
display.drawTextCentered(display.width() / 2, 50, PRESS_LABEL " to open"); display.drawTextCentered(display.width() / 2, content_y + step * 2, PRESS_LABEL " to open");
} else if (_page == HomePage::TOOLS) { } else if (_page == HomePage::TOOLS) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.setTextSize(1); display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 22, "Tools"); display.drawTextCentered(display.width() / 2, content_y, "Tools");
display.drawTextCentered(display.width() / 2, 50, PRESS_LABEL " to open"); display.drawTextCentered(display.width() / 2, content_y + step * 2, PRESS_LABEL " to open");
} else if (_page == HomePage::QUICK_MSG) { } else if (_page == HomePage::QUICK_MSG) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.setTextSize(1); display.setTextSize(1);
display.drawTextCentered(display.width() / 2, 22, "Messages"); display.drawTextCentered(display.width() / 2, content_y, "Messages");
int total_unread = _task->getDMUnreadTotal() + _task->getChannelUnreadCount() + _task->getRoomUnreadCount(); int total_unread = _task->getDMUnreadTotal() + _task->getChannelUnreadCount() + _task->getRoomUnreadCount();
if (total_unread > 0) { if (total_unread > 0) {
char badge[20]; char badge[20];
snprintf(badge, sizeof(badge), "%d unread", total_unread); snprintf(badge, sizeof(badge), "%d unread", total_unread);
display.drawTextCentered(display.width() / 2, 35, badge); display.drawTextCentered(display.width() / 2, content_y + step, badge);
} }
display.drawTextCentered(display.width() / 2, 50, PRESS_LABEL " to open"); display.drawTextCentered(display.width() / 2, content_y + step * 2, PRESS_LABEL " to open");
} else if (_page == HomePage::SHUTDOWN) { } else if (_page == HomePage::SHUTDOWN) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.setTextSize(1); display.setTextSize(1);
if (_shutdown_init) { if (_shutdown_init) {
display.drawTextCentered(display.width() / 2, 34, "hibernating..."); display.drawTextCentered(display.width() / 2, content_y + step, "hibernating...");
} else { } else {
display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32); display.drawXbm((display.width() - 32) / 2, content_y, power_icon, 32, 32);
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL); display.drawTextCentered(display.width() / 2, content_y + 32 + 3, "hibernate:" PRESS_LABEL);
} }
} }
bool auto_adv = _node_prefs && _node_prefs->advert_auto_interval_sec > 0; bool auto_adv = _node_prefs && _node_prefs->advert_auto_interval_sec > 0;
@@ -1337,9 +1355,11 @@ void UITask::loop() {
// Lock screen: clock + unlock hint popup // Lock screen: clock + unlock hint popup
uint32_t unix_ts = rtc_clock.getCurrentTime(); uint32_t unix_ts = rtc_clock.getCurrentTime();
_display->setColor(DisplayDriver::LIGHT); _display->setColor(DisplayDriver::LIGHT);
_display->setTextSize(1);
const int lk_lh = _display->getLineHeight();
const int lk_step = _display->lineStep();
if (unix_ts < 1000000000UL) { if (unix_ts < 1000000000UL) {
_display->setTextSize(1); _display->drawTextCentered(_display->width() / 2, _display->height() / 2 - lk_step, "No time sync");
_display->drawTextCentered(_display->width() / 2, 20, "No time sync");
} else { } else {
int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0; int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0;
unix_ts += (int32_t)tz * 3600; unix_ts += (int32_t)tz * 3600;
@@ -1347,18 +1367,21 @@ void UITask::loop() {
struct tm* ti = gmtime(&t); struct tm* ti = gmtime(&t);
char buf[12]; char buf[12];
_display->setTextSize(2); _display->setTextSize(2);
const int lh2 = _display->getLineHeight(); // sz2 line height
const int clk_y = 2;
if (_node_prefs && _node_prefs->clock_12h) { if (_node_prefs && _node_prefs->clock_12h) {
int h = ti->tm_hour % 12; if (h == 0) h = 12; int h = ti->tm_hour % 12; if (h == 0) h = 12;
sprintf(buf, "%d:%02d %s", h, ti->tm_min, ti->tm_hour < 12 ? "AM" : "PM"); sprintf(buf, "%d:%02d %s", h, ti->tm_min, ti->tm_hour < 12 ? "AM" : "PM");
} else { } else {
sprintf(buf, "%02d:%02d", ti->tm_hour, ti->tm_min); sprintf(buf, "%02d:%02d", ti->tm_hour, ti->tm_min);
} }
_display->drawTextCentered(_display->width() / 2, 8, buf); _display->drawTextCentered(_display->width() / 2, clk_y, buf);
_display->setTextSize(1); _display->setTextSize(1);
static const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; static const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
static const char* mo[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}; static const char* mo[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
sprintf(buf, "%s %d %s", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon]); sprintf(buf, "%s %d %s", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon]);
_display->drawTextCentered(_display->width() / 2, 26, buf); int date_y = clk_y + lh2 + 2;
_display->drawTextCentered(_display->width() / 2, date_y, buf);
// Two sensor values side by side (dashboard_fields[0] and [1]) // Two sensor values side by side (dashboard_fields[0] and [1])
if (_node_prefs) { if (_node_prefs) {
@@ -1375,17 +1398,17 @@ void UITask::loop() {
formatDashVal(f0, v0, sizeof(v0), _batt_mv, lpp_ptr); formatDashVal(f0, v0, sizeof(v0), _batt_mv, lpp_ptr);
formatDashVal(f1, v1, sizeof(v1), _batt_mv, lpp_ptr); formatDashVal(f1, v1, sizeof(v1), _batt_mv, lpp_ptr);
if (v0[0] || v1[0]) { if (v0[0] || v1[0]) {
_display->setTextSize(1); int sv_y = date_y + lk_step;
_display->setColor(DisplayDriver::LIGHT); _display->setColor(DisplayDriver::LIGHT);
if (v0[0] && v1[0]) { if (v0[0] && v1[0]) {
_display->setCursor(0, 37); _display->setCursor(0, sv_y);
_display->print(v0); _display->print(v0);
int vw = _display->getTextWidth(v1); int vw = _display->getTextWidth(v1);
_display->setCursor(_display->width() - vw, 37); _display->setCursor(_display->width() - vw, sv_y);
_display->print(v1); _display->print(v1);
} else { } else {
const char* sv = v0[0] ? v0 : v1; const char* sv = v0[0] ? v0 : v1;
_display->drawTextCentered(_display->width() / 2, 37, sv); _display->drawTextCentered(_display->width() / 2, sv_y, sv);
} }
} }
} }
@@ -1395,11 +1418,11 @@ void UITask::loop() {
const char* hint = _lock_seq_count == 0 ? "Hold Back + 3xEnter" : const char* hint = _lock_seq_count == 0 ? "Hold Back + 3xEnter" :
_lock_seq_count == 1 ? "Enter x2 more..." : "Enter x1 more..."; _lock_seq_count == 1 ? "Enter x2 more..." : "Enter x1 more...";
int p = 3; int p = 3;
int hy = _display->height() - 13; int hy = _display->height() - lk_lh - p * 2;
int hw = _display->getTextWidth(hint); int hw = _display->getTextWidth(hint);
int hx = (_display->width() - hw) / 2; int hx = (_display->width() - hw) / 2;
_display->setColor(DisplayDriver::LIGHT); _display->setColor(DisplayDriver::LIGHT);
_display->fillRect(hx - p, hy - p, hw + p*2, 8 + p*2); _display->fillRect(hx - p, hy - p, hw + p*2, lk_lh + p*2);
_display->setColor(DisplayDriver::DARK); _display->setColor(DisplayDriver::DARK);
_display->setCursor(hx, hy); _display->setCursor(hx, hy);
_display->print(hint); _display->print(hint);
@@ -1457,8 +1480,10 @@ void UITask::loop() {
_display->startFrame(); _display->startFrame();
_display->setTextSize(1); _display->setTextSize(1);
_display->setColor(DisplayDriver::LIGHT); _display->setColor(DisplayDriver::LIGHT);
_display->drawTextCentered(_display->width() / 2, 24, "Low Battery"); int mid = _display->height() / 2;
_display->drawTextCentered(_display->width() / 2, 36, "Shutting down"); int step = _display->lineStep();
_display->drawTextCentered(_display->width() / 2, mid - step, "Low Battery");
_display->drawTextCentered(_display->width() / 2, mid, "Shutting down");
_display->endFrame(); _display->endFrame();
delay(2000); delay(2000);
} }
+10
View File
@@ -31,6 +31,16 @@ public:
virtual int getCharWidth() const { return 6; } // typical character advance width (px) virtual int getCharWidth() const { return 6; } // typical character advance width (px)
virtual int getLineHeight() const { return 8; } // pixel rows per text line virtual int getLineHeight() const { return 8; } // pixel rows per text line
virtual void setLemonFont(bool) { } // no-op; overridden by displays that support Lemon virtual void setLemonFont(bool) { } // no-op; overridden by displays that support Lemon
// Layout helpers — derived from font metrics and screen size.
// Use these instead of hardcoded pixel values so layouts adapt to any display.
int lineStep() const { return getLineHeight() + 2; } // row pitch: text + gap
int headerH() const { return getLineHeight() + 3; } // title bar height
int listStart() const { return headerH(); } // y where list items begin
int listVisible(int itemH) const { return (height() - listStart()) / itemH; }
int listVisible() const { return listVisible(lineStep()); }
// x where a right-side value column starts (leaves ~8 chars for the value)
int valCol() const { return width() - getCharWidth() * 8; }
virtual void drawTextCentered(int mid_x, int y, const char* str) { // helper method (override to optimise) virtual void drawTextCentered(int mid_x, int y, const char* str) { // helper method (override to optimise)
int w = getTextWidth(str); int w = getTextWidth(str);
setCursor(mid_x - w/2, y); setCursor(mid_x - w/2, y);