diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 90c0b3bc..ea79579a 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -26,6 +26,10 @@ class SettingsScreen : public UIScreen { #endif #if FEAT_FULL_REFRESH_SETTING EINK_FULL_REFRESH, + UI_TEXT_SCALE, // Text size: 1x..5x the 6x9 font +#if STATUS_ICONS_SCALED + UI_ICON_SCALE, // Icon size: Auto, 1x..8x +#endif #endif // Sound section SECTION_SOUND, @@ -526,6 +530,18 @@ class SettingsScreen : public UIScreen { uint8_t idx = p ? p->eink_full_refresh_every : 0; if (idx >= EINK_FULL_REFRESH_COUNT) idx = 0; val("Full rfsh", EINK_FULL_REFRESH_LABELS[idx]); + } else if (item == UI_TEXT_SCALE) { + char b[4]; + snprintf(b, sizeof(b), "%ux", (unsigned)_task->getUiFontScale()); + val("Text size", b); +#if STATUS_ICONS_SCALED + } else if (item == UI_ICON_SCALE) { + char b[12]; + const uint8_t v = _task->getStatusIconScale(); + if (v == 0) snprintf(b, sizeof(b), "Auto"); + else snprintf(b, sizeof(b), "%ux", (unsigned)v); + val("Icon size", b); +#endif #endif } else if (item == DM_FILTER) { val("DMs", (p && p->dm_show_all) ? "All" : "Fav"); @@ -1036,6 +1052,21 @@ public: _dirty = true; return true; } + if (_selected == UI_TEXT_SCALE && (left || right || enter)) { + int v = _task->getUiFontScale(); // 1..5, wrapping + v = left ? (v <= 1 ? 5 : v - 1) : (v >= 5 ? 1 : v + 1); + _task->setUiFontScale(v); // saved to /ui_scale immediately + return true; + } +#if STATUS_ICONS_SCALED + if (_selected == UI_ICON_SCALE && (left || right || enter)) { + const int n = STATUS_ICON_SCALE_MAX + 1; // Auto, 1x..8x + int v = _task->getStatusIconScale(); + v = (v + (left ? n - 1 : 1)) % n; + _task->setStatusIconScale(v); // saved to /ui_scale immediately + return true; + } +#endif #endif if (_selected == DM_FILTER && p && (left || right || enter)) { p->dm_show_all = p->dm_show_all ? 0 : 1; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 5a923257..01ee711a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -337,8 +337,8 @@ class HomeScreen : public UIScreen { // First row under the header and the page-icon row (see render()). static int contentTop(DisplayDriver& d) { - const int pg_half = (PAGE_ICON_PX * miniIconScale(d) + 1) / 2; - return d.getLineHeight() + 2 * pg_half + 4; + const int pg_half = (PAGE_ICON_PX * statusIconScale(d) + 1) / 2; + return statusBarH(d) + 2 * pg_half + 4; } // 0..1 eased progress of the running slide, or -1 when none. float slideProgress() { @@ -724,8 +724,11 @@ class HomeScreen : public UIScreen { const int lh = display.getLineHeight(); const int cw = display.getCharWidth(); - const int ind = cw + 2; // single-char indicator width - const int ind_h = display.isSingleFont() ? lh - 2 : lh; + const int sis = statusIconScale(display); // STATUS_ICON_SCALE, or the font's + const int bar_h = statusBarH(display); + const int ind = STATUS_ICONS_SCALED ? 7 * sis + 2 : cw + 2; // indicator slot width + const int ind_h = (bar_h > lh) ? bar_h - 2 : (display.isSingleFont() ? lh - 2 : lh); + const int text_y = (bar_h > lh) ? (bar_h - lh) / 2 : 0; // % / V text centred in a tall bar const int ind_gap = display.pixelScale() > 1 ? 3 : 1; // gap between indicator boxes int battLeftX; @@ -733,17 +736,17 @@ class HomeScreen : public UIScreen { char buf[6]; snprintf(buf, sizeof(buf),"%d%%", pct); battLeftX = display.width() - display.getTextWidth(buf) - 1; - display.setCursor(battLeftX, 0); + display.setCursor(battLeftX, text_y); display.print(buf); } else if (mode == battery::VOLTAGE) { char buf[8]; snprintf(buf, sizeof(buf),"%u.%02uV", batteryMilliVolts / 1000, (batteryMilliVolts % 1000) / 10); battLeftX = display.width() - display.getTextWidth(buf) - 1; - display.setCursor(battLeftX, 0); + display.setCursor(battLeftX, text_y); display.print(buf); } else { // icon — scales with lh, same box height as the status icons beside it (ind_h) const int iconH = ind_h; - const int iconW = lh * 2; + const int iconW = (bar_h > lh) ? ind_h * 2 : lh * 2; const int bm = display.pixelScale() > 1 ? 3 : 2; // inner margin: 3px on a doubled panel, else 2px battLeftX = display.width() - iconW - 3; // The outline with its corner pixels left out, softly rounded. @@ -809,7 +812,8 @@ class HomeScreen : public UIScreen { if (!s.active) continue; int ix = x - ind - ind_gap; if (ix < name_min) break; // out of room — drop this + all lower priority - drawSlotIcon(display, ix, ind, ind_h, *s.icon); + if (STATUS_ICONS_SCALED) drawSlotIconScaled(display, ix, ind, ind_h, *s.icon, sis); + else drawSlotIcon(display, ix, ind, ind_h, *s.icon); x = ix; } return x; @@ -1240,7 +1244,7 @@ public: const int W = d.width(), H = d.height(); d.setColor(DisplayDriver::LIGHT); d.setTextSize(1); - const int lh = d.getLineHeight(), top = lh + 3; + const int lh = d.getLineHeight(), top = statusBarH(d) + 3; const bool hint = Features::IS_EINK || _task->lockHintShown(); const int pill_y = H - lh - 3; // the bottom pill, clear of what's above const int unread = _task->getDMUnreadTotal() + _task->getChannelUnreadCount() + _task->getRoomUnreadCount(); @@ -1476,8 +1480,8 @@ public: // Page-indicator row: small page icons (PAGE_ICON_PX) replace the old dots. Centre and // gap scale with the font so the band clears the header above and content // below (identical to the old lh+4 / +6 dots layout at 1x). - const int pg_half = (PAGE_ICON_PX * miniIconScale(display) + 1) / 2; - const int dots_y = lh + pg_half + 1; // icon-row centre, below the header + const int pg_half = (PAGE_ICON_PX * statusIconScale(display) + 1) / 2; // page icons follow STATUS_ICON_SCALE + const int dots_y = statusBarH(display) + pg_half + 1; // icon-row centre, below the header const int content_y = contentTop(display); // first content row, below the icons const float slide = slideProgress(); // -1, or how far a page turn has got @@ -1492,6 +1496,7 @@ public: display.setColor(DisplayDriver::LIGHT); if (_page != LOCK) { + const int bar_ty = (statusBarH(display) - lh) / 2; // text centred in a tall bar (STATUS_ICON_SCALE) // The time, once the clock is set (the node name before that): the // name is what others see, the time is what you glance at here. char filtered_name[sizeof(_node_prefs->node_name)]; @@ -1517,10 +1522,10 @@ public: char pwr_buf[8]; snprintf(pwr_buf, sizeof(pwr_buf), "%ddB", (int)radio_driver.getTxPower()); int pwr_w = display.getTextWidth(pwr_buf); - display.drawTextEllipsized(0, 0, rightEdge - 2 - pwr_w - 2, filtered_name); - display.drawTextRightAlign(rightEdge - 2, 0, pwr_buf); + display.drawTextEllipsized(0, bar_ty, rightEdge - 2 - pwr_w - 2, filtered_name); + display.drawTextRightAlign(rightEdge - 2, bar_ty, pwr_buf); } else { - display.drawTextEllipsized(0, 0, rightEdge - 2, filtered_name); + display.drawTextEllipsized(0, bar_ty, rightEdge - 2, filtered_name); } } } @@ -1535,7 +1540,7 @@ public: int order[(int)Count]; int n = buildVisibleOrder(order); int curr_vis = 0; for (int i = 0; i < n; i++) if (order[i] == _page) { curr_vis = i; break; } - const int s = miniIconScale(display); + const int s = statusIconScale(display); // STATUS_ICON_SCALE, or the font's const int icon_w = PAGE_ICON_PX * s; int pitch = icon_w + 5 * s; // comfortable spacing if (n > 1) { // shrink to fit if many pages @@ -1560,7 +1565,7 @@ public: if (d2 <= pw - icon_w) display.setColor(DisplayDriver::DARK); // inside the pill else if (d2 < pw + icon_w) { x += pitch; continue; } // half under the gliding pill else display.setColor(DisplayDriver::LIGHT); - if (ic) miniIconDrawCentered(display, x, dots_y, *ic); + if (ic) miniIconDrawCenteredScaled(display, x, dots_y, *ic, s); x += pitch; } display.setColor(DisplayDriver::LIGHT); @@ -2042,6 +2047,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no applyBrightness(); applyRotation(); applyFullRefreshInterval(); + loadUiScale(); // Settings > Display > Text size / Icon size applyAllGpioModes(); // restore persisted pin modes to hardware before any UI/bot use // Locked above (a PIN, or the cover closed) before Home existed to be told. if (_locked) syncLockToHome(); @@ -4047,6 +4053,59 @@ void UITask::applyFullRefreshInterval() { } } +// Text size and Icon size (Settings > Display), in their own /ui_scale file -- +// [text pixel scale (0 = build default), icon scale (0 = Auto)] -- so the +// NodePrefs schema is untouched. +static const char* UI_SCALE_FILE = "/ui_scale"; +static const uint8_t UI_TEXT_SCALE_MAX = 5; + +void UITask::loadUiScale() { + uint8_t b[2] = { 0, statusIconScaleSetting() }; + DataStore* ds = the_mesh.getDataStore(); + if (ds) { + File f = ds->openRead(UI_SCALE_FILE); + if (f) { + f.read(b, 2); + f.close(); + } + } + _ui_font_scale = b[0] <= UI_TEXT_SCALE_MAX ? b[0] : 0; + if (b[1] <= STATUS_ICON_SCALE_MAX) statusIconScaleSetting() = b[1]; + if (_display != NULL && _ui_font_scale) _display->setUiScale(_ui_font_scale); +} + +void UITask::saveUiScale() { + DataStore* ds = the_mesh.getDataStore(); + if (!ds) return; + File f = ds->openWrite(UI_SCALE_FILE); + if (f) { + uint8_t b[2] = { _ui_font_scale, statusIconScaleSetting() }; + f.write(b, 2); + f.close(); + } +} + +uint8_t UITask::getUiFontScale() const { + return _display != NULL ? (uint8_t)_display->pixelScale() : 1; +} + +void UITask::setUiFontScale(uint8_t s) { + if (s < 1 || s > UI_TEXT_SCALE_MAX) s = 1; + _ui_font_scale = s; + if (_display != NULL) _display->setUiScale(s); + _next_refresh = 0; + saveUiScale(); +} + +uint8_t UITask::getStatusIconScale() const { return statusIconScaleSetting(); } + +void UITask::setStatusIconScale(uint8_t s) { + if (s > STATUS_ICON_SCALE_MAX) s = 0; + statusIconScaleSetting() = s; + _next_refresh = 0; // the frame differs, so it's redrawn + saveUiScale(); +} + void UITask::applySoundPrefs() { #ifdef PIN_BUZZER if (_node_prefs) setBuzzerVolumeLevel(_node_prefs->buzzer_volume); // with a sample at the new level diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 993ac5cd..608b00e7 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -62,6 +62,7 @@ class UITask : public UITaskBase, public UiCoreHost { unsigned long _next_refresh, _auto_off; bool _display_was_on = true; // loop(): messagesInView() last time round NodePrefs* _node_prefs; + uint8_t _ui_font_scale = 0; // Settings > Display > Text size (0 = build default) bool _locked; bool _tool_from_home = false; bool messagesInView() const; // Messages on a lit, unlocked display @@ -520,6 +521,14 @@ public: bool savePrefsIfDirty(bool& dirty); void applyRotation(); void applyFullRefreshInterval(); + // Settings > Display > Text size / Icon size. Kept in their own /ui_scale + // file rather than NodePrefs, so the prefs schema (and its size assert) is untouched. + uint8_t getUiFontScale() const; // the text's current pixel scale + void setUiFontScale(uint8_t s); // 1..5: applies + saves + uint8_t getStatusIconScale() const; // 0 = Auto (1.5x text), else n + void setStatusIconScale(uint8_t s); // 0..8: applies + saves + void loadUiScale(); // boot-time restore, from begin() + void saveUiScale(); uint32_t autoOffMillis() const { if (!_node_prefs || _node_prefs->auto_off_secs == 0) return 0; return (uint32_t)_node_prefs->auto_off_secs * 1000UL; diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index fd479f57..88509584 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -91,6 +91,55 @@ inline void miniIconDrawCentered(DisplayDriver& d, int cx, int cy, const MiniIco miniIconDrawTop(d, cx - (ic.w * s) / 2, cy - (ic.h * s) / 2, ic); } +// Home status bar icon scale. Defining STATUS_ICON_SCALE (build flag) turns on +// scalable status icons: the bar's icons, battery and the page-icon row below it +// are drawn at n x their pixel grid, and the bar grows tall enough to hold them -- +// for big panels whose 1-line bar leaves them tiny. Its value is the default for +// Settings > Display > Icon size: 0 = Auto (1.5x the text's pixel scale), else n. +// Unset: icons follow the font, as stock. +#ifdef STATUS_ICON_SCALE + #define STATUS_ICONS_SCALED 1 +#else + #define STATUS_ICONS_SCALED 0 + #define STATUS_ICON_SCALE 0 +#endif +static constexpr int STATUS_ICON_SCALE_MAX = 8; +static constexpr int STATUS_ICON_ROWS = MINI_ICONS_LARGE == 2 ? 9 : 7; // tallest status icon +// The Icon size setting (0 = Auto). One instance across translation units. +inline uint8_t& statusIconScaleSetting() { + static uint8_t v = (STATUS_ICON_SCALE >= 0 && STATUS_ICON_SCALE <= STATUS_ICON_SCALE_MAX) ? STATUS_ICON_SCALE : 0; + return v; +} +inline int statusIconScale(DisplayDriver& d) { + if (!STATUS_ICONS_SCALED) return miniIconScale(d); + int s = statusIconScaleSetting(); + if (s == 0) s = (3 * d.pixelScale() + 1) / 2; // Auto: 1.5x the text + return s < 1 ? 1 : s; +} +// Height of the home status bar: one text line, or the scaled icons if taller. +inline int statusBarH(DisplayDriver& d) { + const int lh = d.getLineHeight(); + const int ih = STATUS_ICON_ROWS * statusIconScale(d) + 4; + return (STATUS_ICONS_SCALED && ih > lh) ? ih : lh; +} +// miniIconDrawCentered at an explicit scale (the home page-icon row's). +inline void miniIconDrawCenteredScaled(DisplayDriver& d, int cx, int cy, const MiniIcon& ic, int s) { + const int x = cx - (ic.w * s) / 2, y = cy - (ic.h * s) / 2; + for (int r = 0; r < ic.h; r++) + for (int c = 0; c < ic.w; c++) + if (ic.rows[r] & (1 << c)) d.fillRect(x + c * s, y + r * s, s, s); +} +// drawSlotIcon at an explicit scale (the status bar's). +inline void drawSlotIconScaled(DisplayDriver& d, int x, int box_w, int box_h, const MiniIcon& ic, int s) { + int ix = x + (box_w - ic.w * s) / 2; + int iy = (box_h - ic.h * s) / 2; + if (ix < x) ix = x; + if (iy < 0) iy = 0; + for (int r = 0; r < ic.h; r++) + for (int c = 0; c < ic.w; c++) + if (ic.rows[r] & (1 << c)) d.fillRect(ix + c * s, iy + r * s, s, s); +} + // Centre a mini-icon inside the slot [x, 0, box_w, box_h] using the current ink // colour (no background). Centres on the box itself, not the text line, so the // glyph sits dead-centre regardless of font line height. diff --git a/solo/wio-tracker-l1-gdeq0426/platformio.ini b/solo/wio-tracker-l1-gdeq0426/platformio.ini index 7f90a27d..9ab3b0ef 100644 --- a/solo/wio-tracker-l1-gdeq0426/platformio.ini +++ b/solo/wio-tracker-l1-gdeq0426/platformio.ini @@ -12,6 +12,12 @@ ; a failed allocation there crashes into a boot loop. So: ; OFFLINE_QUEUE_SIZE 32 (stock e-ink: 128) frees ~17 KB ; MAX_CONTACTS 250 (stock: 350) frees ~18 KB +; - EINK_FONT_SCALE 4: text at 4x the 6x9 font (24x36 cells, ~33 characters +; x 13 lines) -- twice the stock 2x. The default for Settings > Display > +; Text size (1x-5x), which overrides it once changed. +; - STATUS_ICON_SCALE 0: turns on scalable Home status/page icons; 0 = Auto +; (1.5x the text: 6x at 4x text). The default for Settings > Display > +; Icon size (Auto, 1x-8x). ; ; Build: pio run -e WioTrackerL1_GDEQ0426T82_companion_solo_dual [env:WioTrackerL1_GDEQ0426T82_companion_solo_dual] @@ -35,6 +41,8 @@ build_flags = ${nrf52_base.build_flags} -D EINK_DISPLAY_MODEL=GxEPD2_426_GDEQ0426T82 -D DISPLAY_ROTATION=0 -D JOYSTICK_UPRIGHT_ROTATION=0 + -D EINK_FONT_SCALE=4 + -D STATUS_ICON_SCALE=0 -D GPS_BAUD_RATE=9600 -D ENV_PIN_SDA=PIN_WIRE1_SDA -D ENV_PIN_SCL=PIN_WIRE1_SCL diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 0cb3709b..e2578ae6 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -536,6 +536,7 @@ public: virtual void setBrightness(uint8_t level) { } // level 0-4 (min to max), no-op default virtual void setDisplayRotation(uint8_t rot) { } // 0-3, no-op for fixed-orientation displays virtual void setFullRefreshInterval(uint8_t n) { } // e-ink: do full refresh every n partial refreshes (0=never) + virtual void setUiScale(uint8_t font_scale) { } // e-ink: text pixel multiplier, 0 = build default // Sideways slide between two frames, for screens that page left/right. // slideBegin() keeps rows y0.. of the frame on screen now; call it before the // next frame is drawn. slideCompose(dx), at the end of drawing that frame, diff --git a/src/helpers/ui/EinkGfxDisplay.h b/src/helpers/ui/EinkGfxDisplay.h index e52fb1a1..6e983559 100644 --- a/src/helpers/ui/EinkGfxDisplay.h +++ b/src/helpers/ui/EinkGfxDisplay.h @@ -30,6 +30,7 @@ class EinkGfxDisplay : public DisplayDriver { const GFXfont* _font; int _cell_w = 6, _cell_h = 9; // the font's cell, before _px and the text size int _px = 1; // pixel doubling (landscape without EINK_LARGE_FONT) + uint8_t _px_override = 0; // Settings > Display > Text size (0 = build default) int _text_sz = 1; int _cx = 0, _cy = 0; // cursor, top of the text row uint32_t _hash = 0, _shown_hash = 0; @@ -74,6 +75,9 @@ public: dest[dest_size - 1] = '\0'; } bool isEink() override { return true; } + // Runtime text scale (Settings > Display > Text size): re-lays out and forces + // a full redraw. 0 = the build default (EINK_FONT_SCALE). + void setUiScale(uint8_t font_scale) override { _px_override = font_scale; updateLayout(); } void startFrame(Color bkg = DARK) override; void setTextSize(int sz) override; diff --git a/src/helpers/ui/EinkGfxDisplayImpl.h b/src/helpers/ui/EinkGfxDisplayImpl.h index 884e05a9..26457b97 100644 --- a/src/helpers/ui/EinkGfxDisplayImpl.h +++ b/src/helpers/ui/EinkGfxDisplayImpl.h @@ -3,6 +3,12 @@ // the font tables below are static arrays. #include "EinkGfxDisplay.h" +// EINK_FONT_SCALE=: pixel multiplier for the 6x9 font on a landscape panel +// without EINK_LARGE_FONT (default 2). 4 on an 800x480 panel = 24x36 cells, +// ~33 characters x 13 lines. Settings > Display > Text size overrides it at run time. +#ifndef EINK_FONT_SCALE + #define EINK_FONT_SCALE 2 +#endif #include "MiscFixedRenderer.h" #if EINK_LARGE_FONT == 2 #include "MiscFixed9x15Font.h" @@ -20,7 +26,8 @@ EinkGfxDisplay::EinkGfxDisplay(int w, int h, Adafruit_GFX& gfx, uint16_t ink, ui void EinkGfxDisplay::updateLayout() { const bool landscape = width() >= height(); _font = (EINK_LARGE_FONT && landscape) ? &EINK_LARGE_GFXFONT : &MiscFixed; - _px = (!EINK_LARGE_FONT && landscape) ? 2 : 1; + _px = (!EINK_LARGE_FONT && landscape) ? EINK_FONT_SCALE : 1; + if (_px_override) _px = _px_override; _cell_w = miscFixedCellW(*_font); _cell_h = _font->yAdvance; _vw_dirty = true; @@ -87,7 +94,7 @@ void EinkGfxDisplay::fillRect(int x, int y, int w, int h) { void EinkGfxDisplay::drawRect(int x, int y, int w, int h) { mix(x); mix(y); mix(w); mix(h); _gfx.drawRect(x, y, w, h, _curr); - if (_px == 2 && w > 2 && h > 2) _gfx.drawRect(x + 1, y + 1, w - 2, h - 2, _curr); + if (_px >= 2 && w > 2 && h > 2) _gfx.drawRect(x + 1, y + 1, w - 2, h - 2, _curr); } void EinkGfxDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {