feat(ui/clock): stacked big-digit clock on portrait e-ink

On a tall portrait e-ink panel the clock and lock screens wasted most of
the vertical space on a small inline "HH:MM". Render HH and MM on two
lines in a new size-4 font (built-in GFX scaled 7×, ~42×56 px) so the
digits roughly double in height and fill the narrow width. Wide panels
(OLED, landscape e-ink) keep the classic single-line size-2 layout.

- New shared drawClockTime() helper used by both the Clock home page and
  the lock screen; returns the y below the time block so the date and
  dashboard rows flow beneath it.
- GxEPDDisplay: size 4 = built-in font × BIG_TEXT_SCALE (7); getCharWidth /
  getLineHeight / setTextSize handle it; fontAscender stays 0 (built-in is
  top-left origin).
- Centre the big digits on their visible glyph width — the built-in font
  advances 6 px per char but the glyph is 5 px wide, so getTextWidth over-
  reports by one trailing column and the digits would sit ~half a column
  left of centre. AM/PM rendered one size larger (size 2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-06-02 23:32:27 +02:00
parent 49536cb3a5
commit 5b25d1eb32
3 changed files with 73 additions and 24 deletions

View File

@@ -156,6 +156,64 @@ static int battMvToPercent(int mv, int low_mv) {
return pct; return pct;
} }
// Render the time starting at top_y; returns the y just below the time block
// so the caller can flow the date / dashboard rows beneath it.
//
// On a tall portrait panel (e-ink in portrait — height > width) HH and MM are
// stacked on two lines in the huge built-in font (size 4, ~56 px tall) so the
// digits fill the narrow width. On wide panels (OLED, landscape e-ink) the
// classic single-line "HH:MM" at size 2 is kept.
static int drawClockTime(DisplayDriver& d, int top_y, const struct tm* ti,
bool h12, bool show_sec) {
const bool tall = d.height() > d.width(); // true only on portrait e-ink
if (tall) {
int hh = ti->tm_hour;
const char* ap = nullptr;
if (h12) { ap = (hh < 12) ? "AM" : "PM"; hh %= 12; if (hh == 0) hh = 12; }
const int cx = d.width() / 2;
char hbuf[4], mbuf[4];
snprintf(hbuf, sizeof(hbuf), "%02d", hh);
snprintf(mbuf, sizeof(mbuf), "%02d", ti->tm_min);
int y = top_y;
d.setTextSize(4);
const int lhb = d.getLineHeight();
// The built-in GFX font advances 6 px per char but the glyph is only 5 px
// wide, so getTextWidth() over-reports by one trailing blank column and
// drawTextCentered() would bias the digits ~half a column to the left.
// Centre on the visible width (minus that trailing column) instead.
const int trail = d.getCharWidth() / 6; // one built-in column at this size
auto drawBig = [&](const char* s, int yy) {
int w = (int)d.getTextWidth(s) - trail;
d.setCursor(cx - w / 2, yy);
d.print(s);
};
drawBig(hbuf, y); y += lhb + 2;
drawBig(mbuf, y); y += lhb + 2;
if (ap) { d.setTextSize(2); d.drawTextCentered(cx, y, ap); y += d.getLineHeight() + 1; }
d.setTextSize(1);
return y;
}
// Wide layout: single inline line at size 2.
char buf[16];
d.setTextSize(2);
const int lh2 = d.getLineHeight();
if (h12) {
int hh = ti->tm_hour % 12; if (hh == 0) hh = 12;
const char* ap = (ti->tm_hour < 12) ? "AM" : "PM";
if (show_sec) snprintf(buf, sizeof(buf), "%d:%02d:%02d%s", hh, ti->tm_min, ti->tm_sec, ap);
else snprintf(buf, sizeof(buf), "%d:%02d %s", hh, ti->tm_min, ap);
} else {
if (show_sec) snprintf(buf, sizeof(buf), "%02d:%02d:%02d", ti->tm_hour, ti->tm_min, ti->tm_sec);
else snprintf(buf, sizeof(buf), "%02d:%02d", ti->tm_hour, ti->tm_min);
}
d.drawTextCentered(d.width() / 2, top_y, buf);
d.setTextSize(1);
return top_y + lh2 + 2;
}
// ── HomeScreen ──────────────────────────────────────────────────────────────── // ── HomeScreen ────────────────────────────────────────────────────────────────
class HomeScreen : public UIScreen { class HomeScreen : public UIScreen {
enum HomePage { enum HomePage {
@@ -532,26 +590,14 @@ public:
char buf[24]; char buf[24];
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.setTextSize(2);
bool show_sec = !Features::IS_EINK && (!_node_prefs || !_node_prefs->clock_hide_seconds); bool show_sec = !Features::IS_EINK && (!_node_prefs || !_node_prefs->clock_hide_seconds);
bool h12 = _node_prefs && _node_prefs->clock_12h; bool h12 = _node_prefs && _node_prefs->clock_12h;
if (h12) { int date_y = drawClockTime(display, 0, ti, h12, show_sec);
int h = ti->tm_hour % 12; if (h == 0) h = 12;
const char* ap = ti->tm_hour < 12 ? "AM" : "PM";
if (show_sec) snprintf(buf, sizeof(buf),"%d:%02d:%02d%s", h, ti->tm_min, ti->tm_sec, ap);
else snprintf(buf, sizeof(buf),"%d:%02d %s", h, ti->tm_min, ap);
} else {
if (show_sec) snprintf(buf, sizeof(buf),"%02d:%02d:%02d", ti->tm_hour, ti->tm_min, ti->tm_sec);
else snprintf(buf, sizeof(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); 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"};
snprintf(buf, sizeof(buf),"%s %d %s %d", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon], 1900 + ti->tm_year); snprintf(buf, sizeof(buf),"%s %d %s %d", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon], 1900 + ti->tm_year);
int date_y = lh2 + 2;
display.drawTextCentered(display.width() / 2, date_y, buf); display.drawTextCentered(display.width() / 2, date_y, buf);
int sep_y = date_y + lh + 1; int sep_y = date_y + lh + 1;
@@ -1733,21 +1779,13 @@ void UITask::loop() {
time_t t = (time_t)unix_ts; time_t t = (time_t)unix_ts;
struct tm* ti = gmtime(&t); struct tm* ti = gmtime(&t);
char buf[12]; char buf[12];
_display->setTextSize(2);
const int lh2 = _display->getLineHeight(); // sz2 line height
const int clk_y = 2; const int clk_y = 2;
if (_node_prefs && _node_prefs->clock_12h) { bool h12 = _node_prefs && _node_prefs->clock_12h;
int h = ti->tm_hour % 12; if (h == 0) h = 12; int date_y = drawClockTime(*_display, clk_y, ti, h12, /*show_sec*/false);
snprintf(buf, sizeof(buf),"%d:%02d %s", h, ti->tm_min, ti->tm_hour < 12 ? "AM" : "PM");
} else {
snprintf(buf, sizeof(buf),"%02d:%02d", ti->tm_hour, ti->tm_min);
}
_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"};
snprintf(buf, sizeof(buf),"%s %d %s", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon]); snprintf(buf, sizeof(buf),"%s %d %s", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon]);
int date_y = clk_y + lh2 + 2;
_display->drawTextCentered(_display->width() / 2, date_y, buf); _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])

View File

@@ -141,6 +141,12 @@ void GxEPDDisplay::setTextSize(int sz) {
// Size 3 always uses FreeSans18pt for large headings. // Size 3 always uses FreeSans18pt for large headings.
int sc = scale(); int sc = scale();
switch (sz) { switch (sz) {
case 4:
// Huge clock digits: built-in font scaled up. Cursor stays top-left
// (fontAscender returns 0 for the built-in font), so layout maths is plain.
display.setFont(NULL);
display.setTextSize(BIG_TEXT_SCALE);
break;
case 3: case 3:
display.setFont(&FreeSans18pt7b); display.setFont(&FreeSans18pt7b);
display.setTextSize(1); display.setTextSize(1);

View File

@@ -93,16 +93,21 @@ public:
// 1 = FreeSans9pt (lineH=16, charW≈9) // 1 = FreeSans9pt (lineH=16, charW≈9)
// 2 = FreeSansBold12pt (lineH=20, charW≈12) // 2 = FreeSansBold12pt (lineH=20, charW≈12)
// 3 = FreeSans18pt (lineH=28, charW≈17) // 3 = FreeSans18pt (lineH=28, charW≈17)
// Built-in font scale used for size 4 (huge clock digits): 6×8 cell × 7 = 42×56.
static const int BIG_TEXT_SCALE = 7;
// Size 1 scales with orientation (portrait 1×, landscape 2×); size 2 is always 12×16; // Size 1 scales with orientation (portrait 1×, landscape 2×); size 2 is always 12×16;
// size 3 is FreeSans18pt (~17×28). Landscape = width >= height. // size 3 is FreeSans18pt (~17×28); size 4 is the built-in font at 7× (~42×56),
// used only for the stacked HH/MM clock on portrait e-ink. Landscape = width >= height.
int getCharWidth() const override { int getCharWidth() const override {
int sc = scale(); int sc = scale();
if (_text_sz == 4) return 6 * BIG_TEXT_SCALE;
if (_text_sz == 3) return 17; if (_text_sz == 3) return 17;
if (_text_sz == 2) return 12 * sc; if (_text_sz == 2) return 12 * sc;
return (_use_lemon ? 5 : 6) * sc; return (_use_lemon ? 5 : 6) * sc;
} }
int getLineHeight() const override { int getLineHeight() const override {
int sc = scale(); int sc = scale();
if (_text_sz == 4) return 8 * BIG_TEXT_SCALE;
if (_text_sz == 3) return 28; if (_text_sz == 3) return 28;
if (_text_sz == 2) return 16 * sc; if (_text_sz == 2) return 16 * sc;
return (_use_lemon ? 10 : 8) * sc; return (_use_lemon ? 10 : 8) * sc;