fix: adaptive layout for e-ink landscape across all remaining screens

- GxEPDDisplay: sz2 uses 4× scale on landscape (clock 32px vs body 16px)
- UITask: clock date_y and splash date_y computed from lh2 after setTextSize(2)
- UITask: battery/mute/BT/advert indicators scale with lh; muted uses text 'M'
- UITask: hibernate hint split into two lines to prevent wrapping
- ToolsScreen: full refactor — headerH, listStart, lineStep, getCharWidth
- KeyboardWidget: all cell sizes computed from display metrics (cell_w, cell_h, spec_w)
- RingtoneEditorScreen: cell_w = charWidth×2+6 (was hardcoded 18)
- SettingsScreen: BRIGHTNESS hidden on e-ink; renderBar boxes scale with lh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-05-22 18:34:20 +02:00
parent f8fb1dac99
commit 1d8ca25469
7 changed files with 93 additions and 62 deletions

View File

@@ -15,12 +15,6 @@ static const int KB_ROWS_CHAR = 4;
static const int KB_COLS_CHAR = 10;
static const int KB_SPECIAL = 5; // [^] [Sp] [De] [{}] [OK]
static const int KB_MAX_LEN = 139;
static const int KB_CELL_W = 12; // 10 cols × 12px = 120px of 128px display
static const int KB_CELL_H = 9;
static const int KB_TEXT_Y = 0;
static const int KB_SEP_Y = 9;
static const int KB_CHARS_Y = 11;
static const int KB_SPECIAL_Y = 48;
static const int KB_PH_MAX = 12; // max placeholders in list
static const int KB_PH_LEN = 9; // max placeholder string length incl. null
@@ -64,33 +58,44 @@ struct KeyboardWidget {
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
// text preview: last 20 chars + cursor
const int lh = display.getLineHeight();
const int cw = display.getCharWidth();
const int cell_w = display.width() / KB_COLS_CHAR;
const int cell_h = lh + 1;
const int sep_y = lh + 1;
const int chars_y = sep_y + 2;
const int spec_y = chars_y + KB_ROWS_CHAR * cell_h;
const int spec_w = display.width() / KB_SPECIAL;
// text preview: last N chars + cursor (fit within screen width)
int max_preview = display.width() / cw - 1;
if (max_preview > 30) max_preview = 30;
const char* disp_start = buf;
int disp_len = len;
if (disp_len > 20) { disp_start = buf + (disp_len - 20); disp_len = 20; }
char preview[24];
if (disp_len > max_preview) { disp_start = buf + (disp_len - max_preview); disp_len = max_preview; }
char preview[32];
snprintf(preview, sizeof(preview), "%.*s_", disp_len, disp_start);
display.setCursor(0, KB_TEXT_Y);
display.setCursor(0, 0);
display.print(preview);
display.fillRect(0, KB_SEP_Y, display.width(), 1);
display.fillRect(0, sep_y, display.width(), 1);
// character grid
for (int r = 0; r < KB_ROWS_CHAR; r++) {
int y = KB_CHARS_Y + r * KB_CELL_H;
int y = chars_y + r * cell_h;
for (int c = 0; c < KB_COLS_CHAR; c++) {
bool sel = (row == r && col == c);
char ch = KB_CHARS[r][c];
if (caps && ch >= 'a' && ch <= 'z') ch = ch - 'a' + 'A';
char ch_buf[2] = { ch == ' ' ? '_' : ch, '\0' };
int cx = c * KB_CELL_W;
int cx = c * cell_w;
if (sel) {
display.setColor(DisplayDriver::LIGHT);
display.fillRect(cx, y - 1, KB_CELL_W - 1, KB_CELL_H);
display.fillRect(cx, y - 1, cell_w - 1, cell_h);
display.setColor(DisplayDriver::DARK);
} else {
display.setColor(DisplayDriver::LIGHT);
}
display.setCursor(cx + 3, y);
display.setCursor(cx + (cell_w - cw) / 2, y);
display.print(ch_buf);
}
}
@@ -100,15 +105,16 @@ struct KeyboardWidget {
for (int i = 0; i < KB_SPECIAL; i++) {
bool sel = (row == KB_ROWS_CHAR && col == i);
bool active = (i == 0 && caps);
int sx = i * 25;
int sx = i * spec_w;
if (sel || active) {
display.setColor(DisplayDriver::LIGHT);
display.fillRect(sx, KB_SPECIAL_Y - 1, 24, KB_CELL_H);
display.fillRect(sx, spec_y - 1, spec_w - 1, cell_h);
display.setColor(DisplayDriver::DARK);
} else {
display.setColor(DisplayDriver::LIGHT);
}
display.setCursor(sx + 1, KB_SPECIAL_Y);
int tw = display.getTextWidth(spec[i]);
display.setCursor(sx + (spec_w - tw) / 2, spec_y);
display.print(spec[i]);
display.setColor(DisplayDriver::LIGHT);
}

View File

@@ -7,7 +7,6 @@ class RingtoneEditorScreen : public UIScreen {
NodePrefs* _prefs;
static const int MAX_NOTES = 32;
static const int CELL_W = 18;
static const int MENU_VISIBLE = 5;
int _visible_notes = 7; // updated in render(); used by clampScroll()
@@ -93,9 +92,11 @@ public:
display.setColor(DisplayDriver::LIGHT);
const int lh = display.getLineHeight();
const int cw = display.getCharWidth();
const int cell_w = cw * 2 + 6; // fits 2-char label with margin
const int notes_y = display.listStart();
const int cell_h = lh + 6;
_visible_notes = display.width() / CELL_W;
_visible_notes = display.width() / cell_w;
char hdr[32];
snprintf(hdr, sizeof(hdr), "M%d BPM:%u %d/%d", _slot + 1, BPM_OPTS[_bpm_idx], _len, MAX_NOTES);
@@ -105,7 +106,7 @@ public:
for (int i = 0; i < _visible_notes; i++) {
int ni = _scroll + i;
int cx = i * CELL_W;
int cx = i * cell_w;
bool sel = (ni == _cursor);
if (ni < _len) {
uint8_t pitch = notePitch(_notes[ni]);
@@ -120,24 +121,24 @@ public:
}
if (sel) {
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);
} else {
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 + (cell_w - cw * 2) / 2, notes_y + 3);
display.print(label);
display.setColor(DisplayDriver::LIGHT);
} else if (ni == _len && _len < MAX_NOTES) {
if (sel) {
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);
} else {
display.setColor(DisplayDriver::LIGHT);
}
display.setCursor(cx + 6, notes_y + 3);
display.setCursor(cx + (cell_w - cw) / 2, notes_y + 3);
display.print("+");
display.setColor(DisplayDriver::LIGHT);
}

View File

@@ -8,7 +8,9 @@ class SettingsScreen : public UIScreen {
enum SettingItem {
// Display section
SECTION_DISPLAY,
#if !defined(EINK_DISPLAY_MODEL)
BRIGHTNESS,
#endif
AUTO_OFF,
AUTO_LOCK,
BATT_DISPLAY,
@@ -85,7 +87,9 @@ class SettingsScreen : public UIScreen {
}
void renderBar(DisplayDriver& display, int x, int y, int value, int max_val) {
const int box_w = 7, box_h = 7, gap = 2;
const int box_h = display.getLineHeight() - 2;
const int box_w = box_h;
const int gap = 2;
for (int i = 0; i < max_val; i++) {
int bx = x + i * (box_w + gap);
display.drawRect(bx, y, box_w, box_h);
@@ -236,10 +240,13 @@ class SettingsScreen : public UIScreen {
display.print(sel ? ">" : " ");
display.setCursor(display.getCharWidth() + 2, y);
#if !defined(EINK_DISPLAY_MODEL)
if (item == BRIGHTNESS) {
display.print("Bright");
renderBar(display, display.valCol(), y, (p ? p->display_brightness : 2) + 1, 5);
} else if (item == BUZZER) {
} else
#endif
if (item == BUZZER) {
display.print("Buzzer");
display.setCursor(display.valCol(), y);
#ifdef PIN_BUZZER
@@ -357,7 +364,7 @@ class SettingsScreen : public UIScreen {
public:
SettingsScreen(UITask* task)
: _task(task), _selected(BRIGHTNESS), _scroll(0), _visible(4), _dirty(false), _edit_slot(-1) {}
: _task(task), _selected(AUTO_OFF), _scroll(0), _visible(4), _dirty(false), _edit_slot(-1) {}
void markClean() { _dirty = false; }
@@ -441,12 +448,14 @@ public:
bool left = (c == KEY_LEFT || c == KEY_PREV);
bool enter = (c == KEY_ENTER);
#if !defined(EINK_DISPLAY_MODEL)
if (_selected == BRIGHTNESS) {
uint8_t lvl = _task->getBrightnessLevel();
if (right && lvl < 4) { _task->setBrightnessLevel(lvl + 1); _dirty = true; return true; }
if (left && lvl > 0) { _task->setBrightnessLevel(lvl - 1); _dirty = true; return true; }
return right || left;
}
#endif
if (_selected == BUZZER && (left || right || enter)) {
_task->cycleBuzzerMode();
_dirty = true;

View File

@@ -16,21 +16,25 @@ public:
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
display.drawTextCentered(display.width() / 2, 0, "TOOLS");
display.fillRect(0, 10, display.width(), 1);
display.fillRect(0, display.headerH() - 1, display.width(), 1);
int item_h = display.lineStep();
int start_y = display.listStart();
int cw = display.getCharWidth();
for (int i = 0; i < ITEM_COUNT; i++) {
int y = 12 + i * 12;
int y = start_y + i * item_h;
bool sel = (i == _sel);
if (sel) {
display.setColor(DisplayDriver::LIGHT);
display.fillRect(0, y - 1, display.width(), 11);
display.fillRect(0, y - 1, display.width(), item_h);
display.setColor(DisplayDriver::DARK);
} else {
display.setColor(DisplayDriver::LIGHT);
}
display.setCursor(0, y);
display.print(sel ? ">" : " ");
display.setCursor(8, y);
display.setCursor(cw + 2, y);
display.print(ITEMS[i]);
}
display.setColor(DisplayDriver::LIGHT);

View File

@@ -78,10 +78,11 @@ public:
// version info at sz2
int ver_y = logo_y + 13 + 2;
display.setTextSize(2);
int lh2 = display.getLineHeight();
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;
// build date at sz1, below sz2 version
int date_y = ver_y + lh2 + 2;
display.setTextSize(1);
display.drawTextCentered(display.width()/2, date_y, FIRMWARE_BUILD_DATE);
@@ -239,6 +240,10 @@ class HomeScreen : public UIScreen {
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
const int lh = display.getLineHeight();
const int cw = display.getCharWidth();
const int ind = cw + 2; // single-char indicator width
int battLeftX;
if (mode == 1) { // percent
char buf[6];
@@ -252,52 +257,54 @@ class HomeScreen : public UIScreen {
battLeftX = display.width() - display.getTextWidth(buf) - 1;
display.setCursor(battLeftX, 0);
display.print(buf);
} else { // icon
const int iconWidth = 24, iconHeight = 8;
battLeftX = display.width() - iconWidth - 5;
display.drawRect(battLeftX, 0, iconWidth, iconHeight);
display.fillRect(battLeftX + iconWidth, iconHeight / 4, 3, iconHeight / 2);
int fillWidth = (pct * (iconWidth - 4)) / 100;
display.fillRect(battLeftX + 2, 2, fillWidth, iconHeight - 4);
} else { // icon — scales with lh
const int iconH = lh;
const int iconW = iconH * 3;
battLeftX = display.width() - iconW - 3;
display.drawRect(battLeftX, 0, iconW, iconH);
display.fillRect(battLeftX + iconW, iconH / 4, 2, iconH / 2);
int fillW = (pct * (iconW - 4)) / 100;
display.fillRect(battLeftX + 2, 2, fillW, iconH - 4);
}
#ifdef PIN_BUZZER
if (_task->isBuzzerQuiet()) {
display.setColor(DisplayDriver::LIGHT);
display.drawXbm(battLeftX - 9, 0, muted_icon, 8, 8);
int mx = battLeftX - ind - 1;
display.fillRect(mx, 0, ind, lh);
display.setColor(DisplayDriver::DARK);
display.setCursor(mx + 1, 0);
display.print("M");
display.setColor(DisplayDriver::LIGHT);
battLeftX = mx;
}
#endif
// BT connection indicator (left of muted/battery icons)
int leftmostX = battLeftX;
if (_task->isSerialEnabled()) {
#ifdef PIN_BUZZER
int btX = battLeftX - 18;
#else
int btX = battLeftX - 9;
#endif
int btX = battLeftX - ind - 1;
if (_task->hasConnection()) {
display.setColor(DisplayDriver::LIGHT);
display.fillRect(btX - 1, 0, 7, 7);
display.fillRect(btX, 0, ind, lh);
display.setColor(DisplayDriver::DARK);
display.setCursor(btX, 0);
display.setCursor(btX + 1, 0);
display.print("B");
display.setColor(DisplayDriver::LIGHT);
} else {
display.setColor(DisplayDriver::LIGHT);
display.setCursor(btX, 0);
display.setCursor(btX + 1, 0);
display.print("b");
}
leftmostX = btX - 1;
// "A" indicator — left of BT, blinks 50% duty at 1s period
// "A" indicator — left of BT, blinks 50% duty at 4s period
if (_node_prefs && _node_prefs->advert_auto_interval_sec > 0) {
int aX = leftmostX - 8;
int aX = leftmostX - ind;
if ((millis() % 4000) < 2000) {
display.setColor(DisplayDriver::LIGHT);
display.fillRect(aX - 1, 0, 7, 7);
display.fillRect(aX, 0, ind, lh);
display.setColor(DisplayDriver::DARK);
display.setCursor(aX, 0);
display.setCursor(aX + 1, 0);
display.print("A");
display.setColor(DisplayDriver::LIGHT);
}
@@ -413,15 +420,17 @@ public:
if (show_sec) sprintf(buf, "%02d:%02d:%02d", ti->tm_hour, ti->tm_min, ti->tm_sec);
else sprintf(buf, "%02d:%02d", ti->tm_hour, ti->tm_min);
}
int lh2 = display.getLineHeight(); // sz2 height: 16 (OLED) or 32 (landscape)
display.drawTextCentered(display.width() / 2, 0, buf);
display.setTextSize(1);
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"};
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);
int date_y = lh2 + 2;
display.drawTextCentered(display.width() / 2, date_y, buf);
int sep_y = 19 + lh + 1;
int sep_y = date_y + lh + 1;
int dash0 = sep_y + 3;
display.fillRect(0, sep_y, display.width(), 1);
@@ -706,7 +715,9 @@ public:
display.drawTextCentered(display.width() / 2, content_y + step, "hibernating...");
} else {
display.drawXbm((display.width() - 32) / 2, content_y, power_icon, 32, 32);
display.drawTextCentered(display.width() / 2, content_y + 32 + 3, "hibernate:" PRESS_LABEL);
int hint_base = content_y + 32 + 3;
display.drawTextCentered(display.width() / 2, hint_base, "hibernate:");
display.drawTextCentered(display.width() / 2, hint_base + step, PRESS_LABEL);
}
}
bool auto_adv = _node_prefs && _node_prefs->advert_auto_interval_sec > 0;