diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 4833a41b..3c0f0804 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -312,7 +312,7 @@ struct NodePrefs { // persisted to file } } static const char* alarmRepeatLabel(uint8_t idx) { - static const char* L[ALARM_REPEAT_COUNT] = { "Off", "Daily", "Weekdays", "Weekends" }; + static const char* L[ALARM_REPEAT_COUNT] = { "OFF", "Daily", "Weekdays", "Weekends" }; return L[idx < ALARM_REPEAT_COUNT ? idx : 0]; } diff --git a/examples/companion_radio/ui-new/ClockToolsScreen.h b/examples/companion_radio/ui-new/ClockToolsScreen.h index 03772b67..61012709 100644 --- a/examples/companion_radio/ui-new/ClockToolsScreen.h +++ b/examples/companion_radio/ui-new/ClockToolsScreen.h @@ -13,10 +13,14 @@ // • Stopwatch — purely a display utility with no background action, so its // millis() state lives here; it keeps counting while you're elsewhere. // -// Numeric fields (alarm hour/minute, timer h/m/s) are edited with the shared -// framework DigitEditor (digit-by-digit: LEFT/RIGHT cursor, UP/DOWN +/- the -// place, min/max enforced) — the same widget Settings/Repeater use for Freq, -// rather than a hand-rolled wrap. +// Alarm hour/minute and the Timer's HH:MM:SS are both edited with the same +// hand-rolled digit cursor (LEFT/RIGHT moves between digits, UP/DOWN steps +// the digit under it, wrapping within its place and keeping the whole field +// a valid time) rather than the shared DigitEditor framework Settings/ +// Repeater use for Freq — DigitEditor edits one flat decimal value, and a +// combined HH:MM/HH:MM:SS field needs each place capped independently (hour +// tens <= 2, minute/second tens <= 5), which a single flat step size can't +// express without landing on invalid times at the boundaries. // // E-INK: the running timer/stopwatch readouts would thrash a slow e-paper panel // if redrawn every second, so on e-ink the live readout refreshes only coarsely @@ -25,7 +29,6 @@ #include "../NodePrefs.h" #include "icons.h" // drawList / drawRowSelection -#include "DigitEditor.h" // shared digit-by-digit numeric editor class ClockToolsScreen : public UIScreen { UITask* _task; @@ -45,12 +48,10 @@ class ClockToolsScreen : public UIScreen { uint32_t _sw_start_ms = 0; uint32_t _sw_accum_ms = 0; - // Shared numeric editor (alarm hour/minute) + which field it targets. The - // timer uses its own continuous digit cursor (per-place caps a single - // DigitEditor can't express), so it isn't listed here. - enum EditKind : uint8_t { EK_NONE, EK_A_HOUR, EK_A_MIN }; - DigitEditor _editor; - EditKind _edit_kind = EK_NONE; + // Alarm time's own digit cursor (same shape as the timer's, just HH:MM — + // no seconds field). + bool _alarm_editing = false; + uint8_t _alarm_cur = 0; // digit cursor over HH:MM, 0..3 (skips the colon) // E-ink: coarse refresh for the live readouts (millis underneath is exact). static int liveTickMs(DisplayDriver& d, int oled_ms) { return d.isEink() ? 30000 : oled_ms; } @@ -96,32 +97,49 @@ class ClockToolsScreen : public UIScreen { *f = (uint8_t)(t * 10 + u); } - // Open the DigitEditor on a numeric field (2 integer digits, no decimals). - void editField(EditKind kind, uint8_t value, uint8_t vmax) { - _edit_kind = kind; - _editor.begin((float)value, 0.0f, (float)vmax, 2, 0); - } - - // Feed a key to the open editor, write the (live) value back to its field, and - // close the editor when DigitEditor reports DONE/CANCELLED. - bool feedEditor(char c) { - DigitEditor::Result r = _editor.handleInput(c); - uint8_t v = (uint8_t)(_editor.value + 0.5f); - switch (_edit_kind) { - case EK_A_HOUR: _prefs->alarm_hour = v; _alarm_dirty = true; _task->onAlarmChanged(); break; - case EK_A_MIN: _prefs->alarm_min = v; _alarm_dirty = true; _task->onAlarmChanged(); break; - default: break; + // Step the digit under the alarm-time cursor by dir (+1/-1) — same shape as + // stepTimerDigit, just two fields (hour/minute) instead of three. + void stepAlarmDigit(int dir) { + uint8_t* f; int tens_max; bool is_hours; + if (_alarm_cur < 2) { f = &_prefs->alarm_hour; tens_max = 2; is_hours = true; } + else { f = &_prefs->alarm_min; tens_max = 5; is_hours = false; } + int t = *f / 10, u = *f % 10; + if (_alarm_cur % 2 == 0) { // tens place + t = (t + dir + (tens_max + 1)) % (tens_max + 1); + if (is_hours && t == 2 && u > 3) u = 3; // 2X hours can't exceed 23 + } else { // units place + int umax = (is_hours && t == 2) ? 3 : 9; + u = (u + dir + (umax + 1)) % (umax + 1); } - if (r != DigitEditor::NONE) _edit_kind = EK_NONE; // editor closed - return true; + *f = (uint8_t)(t * 10 + u); + _alarm_dirty = true; + _task->onAlarmChanged(); } - // Draw a row's value column: the live editor when this row is the one being - // edited, otherwise the static text. - void drawValue(DisplayDriver& d, int y, int valx, int reserve, bool sel, - EditKind mykind, const char* text) { - if (sel && _editor.active && _edit_kind == mykind) _editor.render(d, valx, y); - else d.drawTextEllipsized(valx, y, d.width() - valx - reserve, text); + // Draw the alarm's "HH:MM" value: while this row is in its digit-cursor + // sub-mode, the digit under the cursor is shown inverted (same convention as + // DigitEditor::render — assumes the caller is already inside a selected row, + // ink DARK on a LIGHT selection bar); otherwise just the plain text. + void drawAlarmTime(DisplayDriver& d, int y, int valx, bool editing) { + char buf[6]; + fmtClock(buf, sizeof(buf), _prefs->alarm_hour, _prefs->alarm_min); + if (!editing) { d.setCursor(valx, y); d.print(buf); return; } + const int cw = d.getCharWidth(); + const int char_idx = _alarm_cur + (_alarm_cur / 2); // skip the colon + for (int k = 0; buf[k]; k++) { + int cx = valx + k * cw; + if (k == char_idx) { + d.setColor(DisplayDriver::DARK); + d.fillRect(cx, y - 1, cw, d.getLineHeight() + 1); + d.setColor(DisplayDriver::LIGHT); + } else { + d.setColor(DisplayDriver::DARK); + } + char one[2] = { buf[k], '\0' }; + d.setCursor(cx, y); + d.print(one); + } + d.setColor(DisplayDriver::LIGHT); } // ── Sub-renders ─────────────────────────────────────────────────────────── @@ -145,21 +163,22 @@ class ClockToolsScreen : public UIScreen { int renderAlarm(DisplayDriver& d) { d.drawCenteredHeader("ALARM"); if (!_prefs) return 60000; - const char* rows[4] = { "Hour", "Minute", "Repeat", "Armed" }; - if (_sel > 3) _sel = 3; + const char* rows[3] = { "Time", "Repeat", "Armed" }; + if (_sel > 2) _sel = 2; const int valx = d.width() / 2 + 6; - drawList(d, 4, _sel, _scroll, [&](int i, int y, bool sel, int reserve) { + drawList(d, 3, _sel, _scroll, [&](int i, int y, bool sel, int reserve) { drawRowSelection(d, y, sel, reserve); d.setCursor(4, y); d.print(rows[i]); - char b[10]; - if (i == 0) snprintf(b, sizeof(b), "%02u", _prefs->alarm_hour); - else if (i == 1) snprintf(b, sizeof(b), "%02u", _prefs->alarm_min); - else if (i == 2) snprintf(b, sizeof(b), "%s", NodePrefs::alarmRepeatLabel(NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask))); - else snprintf(b, sizeof(b), "%s", _prefs->alarm_on ? "ON" : "OFF"); - EditKind k = (i == 0) ? EK_A_HOUR : (i == 1) ? EK_A_MIN : EK_NONE; - drawValue(d, y, valx, reserve, sel, k, b); + if (i == 0) { + drawAlarmTime(d, y, valx, sel && _alarm_editing); + } else if (i == 1) { + d.drawTextEllipsized(valx, y, d.width() - valx - reserve, + NodePrefs::alarmRepeatLabel(NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask))); + } else { + d.drawTextEllipsized(valx, y, d.width() - valx - reserve, _prefs->alarm_on ? "ON" : "OFF"); + } }); - return _editor.active ? 50 : 60000; + return _alarm_editing ? 50 : 60000; } int renderTimer(DisplayDriver& d) { @@ -220,25 +239,50 @@ class ClockToolsScreen : public UIScreen { bool inputAlarm(char c) { if (!_prefs) { if (c == KEY_CANCEL) { _view = V_MENU; _sel = 0; } return true; } - if (_editor.active) return feedEditor(c); + + // Time row's digit-cursor sub-mode (same shape as the Timer's HH:MM:SS + // cursor): LEFT/RIGHT moves across HH:MM's 4 digits, UP/DOWN steps the + // digit under the cursor, Enter/Cancel commits and leaves (there's no + // rollback — every step already writes straight to alarm_hour/min and + // reschedules, same as the old per-field editor did). + if (_alarm_editing) { + if (keyIsPrev(c)) { _alarm_cur = (_alarm_cur + 3) % 4; return true; } + if (keyIsNext(c)) { _alarm_cur = (_alarm_cur + 1) % 4; return true; } + if (c == KEY_UP) { stepAlarmDigit(+1); return true; } + if (c == KEY_DOWN) { stepAlarmDigit(-1); return true; } + if (c == KEY_ENTER || c == KEY_CANCEL) { _alarm_editing = false; return true; } + return true; + } + if (c == KEY_CANCEL) { _task->savePrefsIfDirty(_alarm_dirty); // persist alarm fields only if edited _task->onAlarmChanged(); // re-schedule from the new time _view = V_MENU; _sel = 0; _scroll = 0; return true; } - if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : 3; return true; } - if (c == KEY_DOWN) { _sel = (_sel < 3) ? _sel + 1 : 0; return true; } - if (c == KEY_ENTER) { - if (_sel == 0) editField(EK_A_HOUR, _prefs->alarm_hour, 23); - else if (_sel == 1) editField(EK_A_MIN, _prefs->alarm_min, 59); - else if (_sel == 2) { // Repeat: cycle Off -> Daily -> Weekdays -> Weekends -> Off - uint8_t idx = (NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask) + 1) % NodePrefs::ALARM_REPEAT_COUNT; - _prefs->alarm_repeat_mask = NodePrefs::alarmRepeatMaskForIdx(idx); - _alarm_dirty = true; _task->onAlarmChanged(); - } else { _prefs->alarm_on ^= 1; _alarm_dirty = true; _task->onAlarmChanged(); } + if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : 2; return true; } + if (c == KEY_DOWN) { _sel = (_sel < 2) ? _sel + 1 : 0; return true; } + // Repeat: LEFT/RIGHT steps Off -> Daily -> Weekdays -> Weekends (wrapping), + // matching the rest of the UI's convention for multi-value fields (e.g. + // Settings' Keyboard Alphabet / Battery display); Enter still advances one + // step too, for parity with those same fields. + if (_sel == 1 && (keyIsPrev(c) || keyIsNext(c) || c == KEY_ENTER)) { + int idx = NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask); + idx = keyIsPrev(c) ? (idx + NodePrefs::ALARM_REPEAT_COUNT - 1) % NodePrefs::ALARM_REPEAT_COUNT + : (idx + 1) % NodePrefs::ALARM_REPEAT_COUNT; + _prefs->alarm_repeat_mask = NodePrefs::alarmRepeatMaskForIdx(idx); + _alarm_dirty = true; _task->onAlarmChanged(); return true; } + // Armed: a plain on/off toggle, but still answers LEFT/RIGHT as well as + // Enter (same as every boolean field in Settings — e.g. Auto-lock, Units, + // Power save — flips either way rather than reserving direction for + // "increase/decrease"). + if (_sel == 2 && (keyIsPrev(c) || keyIsNext(c) || c == KEY_ENTER)) { + _prefs->alarm_on ^= 1; _alarm_dirty = true; _task->onAlarmChanged(); + return true; + } + if (c == KEY_ENTER && _sel == 0) { _alarm_editing = true; _alarm_cur = 0; return true; } return true; } @@ -278,7 +322,7 @@ public: void onShow() override { _view = V_MENU; _sel = 0; _scroll = 0; _timer_cur = 0; - _editor.active = false; _edit_kind = EK_NONE; + _alarm_editing = false; } int render(DisplayDriver& display) override { diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 879ea477..d519bf0c 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -459,13 +459,18 @@ class HomeScreen : public UIScreen { battLeftX = display.width() - display.getTextWidth(buf) - 1; display.setCursor(battLeftX, 0); display.print(buf); - } else { // icon — scales with lh - const int iconH = lh; + } 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 bm = display.isLandscape() ? 3 : 2; // inner margin: 3px on landscape e-ink, 2px on OLED/portrait battLeftX = display.width() - iconW - 3; display.drawRect(battLeftX, 0, iconW, iconH); - display.fillRect(battLeftX + iconW, iconH / 4, 2, iconH / 2); + // Nub height/2, vertically centred by remaining-space/2 rather than a flat + // iconH/4 margin — the flat form only centres when iconH is a multiple of + // 4 (true for the old built-in font's lh=8, false for misc-fixed's 7/9), + // so it visibly drifted off-centre once the box height changed. + const int nub_h = iconH / 2; + display.fillRect(battLeftX + iconW, (iconH - nub_h) / 2, 2, nub_h); int fillW = (pct * (iconW - 2 * bm)) / 100; display.fillRect(battLeftX + bm, bm, fillW, iconH - 2 * bm); } diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index d343be29..2c0e6352 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -266,7 +266,7 @@ MINI_ICON(ICON_PG_SENSORS, 5, // thermometer/gauge — sensors MINI_ICON(ICON_PG_SETTINGS, 5, // small cog — settings packRow(".#.#."), packRow("#####"), - packRow("##.##"), + packRow(".#.#."), packRow("#####"), packRow(".#.#.")); MINI_ICON(ICON_PG_MAP, 5, // folded map — map page diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 5e45ac4a..bf8db41f 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -19,7 +19,7 @@ // the two conventions match. static int fontAscender(int sz, bool use_lemon, int scale) { if (sz == 3) return 26; // FreeSans18pt7b: proportional, baseline origin - if (sz == 1 && use_lemon) return 8 * scale; // Lemon GFX font: baseline origin, ascender 8px×scale + if (sz == 1 && use_lemon) return 7 * scale; // misc-fixed 6x9 GFX font: baseline origin, ascent 7px×scale return 0; // GFX built-in font: cursor is top-left of cell } @@ -47,11 +47,11 @@ int16_t GxEPDDisplay::drawLemonChar(int16_t x, int16_t y, uint32_t cp, int sc) { return x + xa * sc; } } - if (cp < Lemon.first || cp > Lemon.last) { - if (cp >= 0x20) display.fillRect(x + sc, y - 8*sc, 4*sc, 6*sc, _curr_color); + if (cp < MiscFixed.first || cp > MiscFixed.last) { + if (cp >= 0x20) display.fillRect(x + sc, y - 7*sc, 4*sc, 6*sc, _curr_color); return x + 6 * sc; } - const GFXglyph* g = &lemonGlyphs[cp - Lemon.first]; + const GFXglyph* g = &MiscFixedGlyphs[cp - MiscFixed.first]; uint8_t w = pgm_read_byte(&g->width), h = pgm_read_byte(&g->height); int8_t xo = (int8_t)pgm_read_byte(&g->xOffset), yo = (int8_t)pgm_read_byte(&g->yOffset); uint8_t xa = pgm_read_byte(&g->xAdvance); @@ -59,7 +59,7 @@ int16_t GxEPDDisplay::drawLemonChar(int16_t x, int16_t y, uint32_t cp, int sc) { uint8_t bits = 0, bit = 0; for (uint8_t row = 0; row < h; row++) for (uint8_t col = 0; col < w; col++) { - if (!bit) { bits = pgm_read_byte(&lemonBitmaps[bo++]); bit = 0x80; } + if (!bit) { bits = pgm_read_byte(&MiscFixedBitmaps[bo++]); bit = 0x80; } if (bits & bit) { if (sc == 1) display.drawPixel(x + xo + col, y + yo + row, _curr_color); else display.fillRect(x + xo*sc + col*sc, y + yo*sc + row*sc, sc, sc, _curr_color); @@ -71,8 +71,8 @@ int16_t GxEPDDisplay::drawLemonChar(int16_t x, int16_t y, uint32_t cp, int sc) { uint8_t GxEPDDisplay::lemonXAdvance(uint32_t cp, int sc) { uint8_t xa; - if (cp < Lemon.first || cp > Lemon.last) xa = 6; - else xa = pgm_read_byte(&lemonGlyphs[cp - Lemon.first].xAdvance); + if (cp < MiscFixed.first || cp > MiscFixed.last) xa = 6; + else xa = pgm_read_byte(&MiscFixedGlyphs[cp - MiscFixed.first].xAdvance); return xa * sc; } @@ -127,7 +127,7 @@ void GxEPDDisplay::startFrame(Color bkg) { display.setTextColor(_curr_color = GxEPD_BLACK); _text_sz = 1; int sc = scale(); - display.setFont(_use_lemon ? &Lemon : NULL); + display.setFont(_use_lemon ? &MiscFixed : NULL); display.setTextSize(sc); display_crc.reset(); } @@ -156,7 +156,7 @@ void GxEPDDisplay::setTextSize(int sz) { display.setTextSize(scale() * 2); break; default: - display.setFont(_use_lemon ? &Lemon : NULL); + display.setFont(_use_lemon ? &MiscFixed : NULL); display.setTextSize(sc); break; } @@ -183,7 +183,7 @@ void GxEPDDisplay::setCursor(int x, int y) { void GxEPDDisplay::print(const char* str) { display_crc.update(str, strlen(str)); - // Lemon path only for sz=1 — setTextSize(2/3) switches GFX to non-Lemon fonts. + // misc-fixed path only for sz=1 — setTextSize(2/3) switches GFX to other fonts. if (_use_lemon && _text_sz == 1) { int16_t cx = display.getCursorX(); int16_t cy = display.getCursorY(); @@ -191,7 +191,7 @@ void GxEPDDisplay::print(const char* str) { const uint8_t* p = (const uint8_t*)str; while (*p) { uint32_t cp = decodeCodepoint(p); - if (cp == '\n') { cy += Lemon.yAdvance * sc; cx = 0; } + if (cp == '\n') { cy += MiscFixed.yAdvance * sc; cx = 0; } else { cx = drawLemonChar(cx, cy, cp, sc); } } display.setCursor(cx, cy); diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index c8837f48..bbc6a8cb 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -23,7 +23,7 @@ #include #include "DisplayDriver.h" -#include "LemonFont.h" +#include "MiscFixedFont.h" #ifndef DISPLAY_ROTATION #define DISPLAY_ROTATION 0 @@ -57,7 +57,7 @@ class GxEPDDisplay : public DisplayDriver { #endif bool _init = false; bool _isOn = false; - bool _use_lemon = false; + bool _use_lemon = true; // e-ink is single-font too now (misc-fixed 6x9), matching the OLED driver uint16_t _curr_color; CRC32 display_crc; int last_display_crc_value = 0; @@ -103,16 +103,16 @@ public: if (_text_sz == 4) return 6 * BIG_TEXT_SCALE; if (_text_sz == 3) return 17; if (_text_sz == 2) return 12 * sc; - return (_use_lemon ? 5 : 6) * sc; + return (_use_lemon ? 6 : 6) * sc; // misc-fixed 6x9 is 6px wide } int getLineHeight() const override { int sc = scale(); if (_text_sz == 4) return 8 * BIG_TEXT_SCALE; if (_text_sz == 3) return 28; if (_text_sz == 2) return 16 * sc; - return (_use_lemon ? 10 : 8) * sc; + return (_use_lemon ? 9 : 8) * sc; // misc-fixed 6x9 box height } - void setLemonFont(bool enabled) override { _use_lemon = enabled; _vw_dirty = true; } + void setLemonFont(bool) override { } // single-font: ignore toggles, stay misc-fixed 6x9 bool isLemonFont() const override { return _use_lemon; } void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override { if (_use_lemon) { diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index e898adc9..9ff3c983 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -160,6 +160,7 @@ build_unflags = -Ofast build_flags = ${WioTrackerL1CompanionDual.build_flags} -D UI_HAS_JOYSTICK_UPDOWN=1 -D ENABLE_SCREENSHOT + -D DUAL_SERIAL=1 -Os extra_scripts = post:create-uf2.py