diff --git a/examples/companion_radio/ui-new/AutoAdvertScreen.h b/examples/companion_radio/ui-new/AutoAdvertScreen.h index c8dc1bfa..50ca594c 100644 --- a/examples/companion_radio/ui-new/AutoAdvertScreen.h +++ b/examples/companion_radio/ui-new/AutoAdvertScreen.h @@ -31,7 +31,7 @@ public: int tip_y = bar_y + bar_h + 4; display.drawTextCentered(display.width() / 2, 0, "AUTO-ADVERT"); - display.fillRect(0, display.headerH() - 1, display.width(), 1); + display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); display.setCursor(2, label_y); display.print("Interval:"); diff --git a/examples/companion_radio/ui-new/BotScreen.h b/examples/companion_radio/ui-new/BotScreen.h index 027f7dfb..1d14d76d 100644 --- a/examples/companion_radio/ui-new/BotScreen.h +++ b/examples/companion_radio/ui-new/BotScreen.h @@ -55,12 +55,14 @@ public: return _kb.render(display); } + int avail_h = display.height() - display.listStart(); int item_h = display.lineStep(); + if (item_h * ITEM_COUNT > avail_h) item_h = avail_h / ITEM_COUNT; int start_y = display.listStart(); int val_x = display.valCol(); display.drawTextCentered(display.width() / 2, 0, "AUTO-REPLY BOT"); - display.fillRect(0, display.headerH() - 1, display.width(), 1); + display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); static const char* labels[] = { "Enable", "Channel", "Trigger", "Reply DM", "Reply Ch" }; for (int i = 0; i < ITEM_COUNT; i++) { diff --git a/examples/companion_radio/ui-new/DashboardConfigScreen.h b/examples/companion_radio/ui-new/DashboardConfigScreen.h index b60011b3..15957c5d 100644 --- a/examples/companion_radio/ui-new/DashboardConfigScreen.h +++ b/examples/companion_radio/ui-new/DashboardConfigScreen.h @@ -46,7 +46,7 @@ public: int val_x = display.valCol(); display.drawTextCentered(display.width() / 2, 0, "CLOCK FIELDS"); - display.fillRect(0, display.headerH() - 1, display.width(), 1); + display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); static const char* labels[] = { "Field 1", "Field 2", "Field 3" }; for (int i = 0; i < FIELD_SLOTS; i++) { diff --git a/examples/companion_radio/ui-new/KeyboardWidget.h b/examples/companion_radio/ui-new/KeyboardWidget.h index ec1c89c6..81560b7b 100644 --- a/examples/companion_radio/ui-new/KeyboardWidget.h +++ b/examples/companion_radio/ui-new/KeyboardWidget.h @@ -61,22 +61,39 @@ struct KeyboardWidget { const int lh = display.getLineHeight(); const int cw = display.getCharWidth(); const int cell_w = display.width() / KB_COLS_CHAR; - const int sep_y = lh; // tight separator: preview row height only + // compact: don't stretch cells beyond lh; freed vertical space goes to preview lines + const int kb_h = (KB_ROWS_CHAR + 1) * lh; + const int preview_h = display.height() - kb_h - 1; + const int prev_lines = (preview_h / lh) > 1 ? (preview_h / lh) : 1; + const int sep_y = prev_lines * lh; const int chars_y = sep_y + 1; const int cell_h = (display.height() - chars_y) / (KB_ROWS_CHAR + 1); 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 > 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, 0); - display.print(preview); + // multi-line text preview: cursor always on last preview line + int cpl = display.width() / cw; // chars per preview line + if (cpl < 1) cpl = 1; + int cursor_line = len / cpl; + int first_line = (cursor_line >= prev_lines) ? (cursor_line - prev_lines + 1) : 0; + int start = first_line * cpl; + for (int pl = 0; pl < prev_lines; pl++) { + int ps = start + pl * cpl; + int pe = ps + cpl; + bool cursor_here = (ps <= len && (len < pe || pl == prev_lines - 1)); + char linebuf[32]; + if (cursor_here) { + int nc = len - ps; if (nc < 0) nc = 0; if (nc > cpl - 1) nc = cpl - 1; + snprintf(linebuf, sizeof(linebuf), "%.*s_", nc, buf + ps); + } else if (len > ps) { + int nc = (len < pe) ? (len - ps) : cpl; + snprintf(linebuf, sizeof(linebuf), "%.*s", nc, buf + ps); + } else { + linebuf[0] = '\0'; + } + display.setCursor(0, pl * lh); + display.print(linebuf); + } display.fillRect(0, sep_y, display.width(), 1); // character grid diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index b8e4e71d..699de8c2 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -234,8 +234,9 @@ class NearbyScreen : public UIScreen { display.print(b64_line); } - // distribute 4 remaining lines across available height below b64 - int step = (display.height() - hdr) / 5; + // distribute 4 remaining lines below b64 — compact step, shrink only if needed + int step = display.lineStep(); + if (step * 5 > display.height() - hdr) step = (display.height() - hdr) / 5; char buf[32]; snprintf(buf, sizeof(buf), "RSSI: %d dBm", (int)r.rssi); display.setCursor(2, hdr + step); display.print(buf); @@ -419,8 +420,9 @@ public: display.drawTextEllipsized(2, 1, display.width() - 4, filtered); display.setColor(DisplayDriver::LIGHT); - // 5 lines: lat, lon, dist+bearing, type, seen — distributed across available height - int step = (display.height() - hdr) / 5; + // 5 lines: lat, lon, dist+bearing, type, seen — compact step, shrink only if needed + int step = display.lineStep(); + if (step * 5 > display.height() - hdr) step = (display.height() - hdr) / 5; char buf[32]; snprintf(buf, sizeof(buf), "Lat: %.5f", e.lat_e6 / 1e6); display.setCursor(2, hdr); display.print(buf); @@ -459,7 +461,7 @@ public: char title[22]; snprintf(title, sizeof(title), "NEARBY[%s]", FILTER_LABELS[_filter]); display.drawTextCentered(display.width() / 2, 0, title); - display.fillRect(0, display.headerH() - 1, display.width(), 1); + display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); if (_count == 0) { display.drawTextCentered(display.width() / 2, display.height() / 2 - display.lineStep() / 2, "No contacts found"); diff --git a/examples/companion_radio/ui-new/QuickMsgScreen.h b/examples/companion_radio/ui-new/QuickMsgScreen.h index 86c25d5d..6b8957aa 100644 --- a/examples/companion_radio/ui-new/QuickMsgScreen.h +++ b/examples/companion_radio/ui-new/QuickMsgScreen.h @@ -468,7 +468,7 @@ public: if (_phase == MODE_SELECT) { display.drawTextCentered(display.width()/2, 0, "MESSAGE"); - display.fillRect(0, display.headerH() - 1, display.width(), 1); + display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); const char* opts[] = { "Direct message", "Channels", "Room Servers" }; int badges[3] = { getDMUnreadTotal(), @@ -501,7 +501,7 @@ public: } else if (_phase == CONTACT_PICK) { display.drawTextCentered(display.width()/2, 0, _room_mode ? "SELECT ROOM" : "SELECT CONTACT"); - display.fillRect(0, display.headerH() - 1, display.width(), 1); + display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); if (_num_contacts == 0) { display.drawTextCentered(display.width()/2, display.height()/2, _room_mode ? "No room servers" : "No favourites"); @@ -551,7 +551,7 @@ public: } else if (_phase == CHANNEL_PICK) { display.drawTextCentered(display.width()/2, 0, "SELECT CHANNEL"); - display.fillRect(0, display.headerH() - 1, display.width(), 1); + display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); if (_num_channels == 0) { display.drawTextCentered(display.width()/2, display.height()/2, "No channels"); @@ -618,15 +618,16 @@ public: 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; + int btn_h = lh + 4; + _hist_visible = (display.height() - hist_start_y - btn_h) / hist_item_h; if (_hist_visible < 1) _hist_visible = 1; - int cby = hist_start_y + _hist_visible * hist_item_h + 2; + int cby = display.height() - lh - 2; char title[24]; display.setColor(DisplayDriver::LIGHT); snprintf(title, sizeof(title), "%.23s", filtered_name); display.drawTextCentered(display.width()/2, 0, title); - display.fillRect(0, lh + 1, display.width(), 1); + display.fillRect(0, lh + 1, display.width(), display.sepH()); int dm_count = dmHistCountForContact(_sel_contact.id.pub_key); @@ -720,16 +721,17 @@ public: 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; + int btn_h = lh + 4; + _hist_visible = (display.height() - hist_start_y - btn_h) / hist_item_h; if (_hist_visible < 1) _hist_visible = 1; - int cby = hist_start_y + _hist_visible * hist_item_h + 2; + int cby = display.height() - lh - 2; ChannelDetails ch; the_mesh.getChannel(_sel_channel_idx, ch); char title[24]; snprintf(title, sizeof(title), "%.23s", ch.name); display.drawTextCentered(display.width()/2, 0, title); - display.fillRect(0, lh + 1, display.width(), 1); + display.fillRect(0, lh + 1, display.width(), display.sepH()); int ch_hist_count = histCountForChannel(_sel_channel_idx); @@ -832,7 +834,7 @@ public: snprintf(title, sizeof(title), "TO:%.14s", _sel_contact.name); } display.drawTextCentered(display.width()/2, 0, title); - display.fillRect(0, display.headerH() - 1, display.width(), 1); + display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); int total_msg_items = 1 + _active_msg_count; for (int i = 0; i < _visible && (_msg_scroll+i) < total_msg_items; i++) { diff --git a/examples/companion_radio/ui-new/RingtoneEditorScreen.h b/examples/companion_radio/ui-new/RingtoneEditorScreen.h index 66801e9a..ddd30d8c 100644 --- a/examples/companion_radio/ui-new/RingtoneEditorScreen.h +++ b/examples/companion_radio/ui-new/RingtoneEditorScreen.h @@ -102,7 +102,7 @@ public: snprintf(hdr, sizeof(hdr), "M%d BPM:%u %d/%d", _slot + 1, BPM_OPTS[_bpm_idx], _len, MAX_NOTES); display.setCursor(0, 0); display.print(hdr); - display.fillRect(0, display.headerH() - 1, display.width(), 1); + display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); for (int i = 0; i < _visible_notes; i++) { int ni = _scroll + i; diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 49aef7ed..b01bf548 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -11,7 +11,9 @@ class SettingsScreen : public UIScreen { #if !defined(EINK_DISPLAY_MODEL) BRIGHTNESS, #endif +#if AUTO_OFF_MILLIS > 0 AUTO_OFF, +#endif AUTO_LOCK, BATT_DISPLAY, #if !defined(EINK_DISPLAY_MODEL) @@ -64,9 +66,11 @@ class SettingsScreen : public UIScreen { int _visible; // items fitting on screen; updated each render, used by handleInput bool _dirty; +#if AUTO_OFF_MILLIS > 0 static const uint16_t AUTO_OFF_OPTS[5]; static const char* AUTO_OFF_LABELS[5]; static const int AUTO_OFF_COUNT = 5; +#endif #if ENV_INCLUDE_GPS == 1 static const uint32_t GPS_INTERVAL_OPTS[6]; static const char* GPS_INTERVAL_LABELS[6]; @@ -101,6 +105,7 @@ class SettingsScreen : public UIScreen { } } +#if AUTO_OFF_MILLIS > 0 int autoOffIndex() { NodePrefs* p = _task->getNodePrefs(); if (!p) return 1; @@ -108,6 +113,7 @@ class SettingsScreen : public UIScreen { if (AUTO_OFF_OPTS[i] == p->auto_off_secs) return i; return 1; } +#endif #if ENV_INCLUDE_GPS == 1 int gpsIntervalIndex() { @@ -289,10 +295,12 @@ class SettingsScreen : public UIScreen { sprintf(buf, "%ddBm", p ? p->tx_power_dbm : 0); display.setCursor(display.valCol(), y); display.print(buf); +#if AUTO_OFF_MILLIS > 0 } else if (item == AUTO_OFF) { display.print("AutoOff"); display.setCursor(display.valCol(), y); display.print(AUTO_OFF_LABELS[autoOffIndex()]); +#endif } else if (item == AUTO_LOCK) { display.print("AutoLock"); display.setCursor(display.valCol(), y); @@ -367,7 +375,7 @@ class SettingsScreen : public UIScreen { public: SettingsScreen(UITask* task) - : _task(task), _selected(AUTO_OFF), _scroll(0), _visible(4), _dirty(false), _edit_slot(-1) {} + : _task(task), _selected(AUTO_LOCK), _scroll(0), _visible(4), _dirty(false), _edit_slot(-1) {} void markClean() { _dirty = false; } @@ -385,7 +393,7 @@ public: display.setColor(DisplayDriver::LIGHT); display.drawTextCentered(display.width() / 2, 0, "SETTINGS"); - display.fillRect(0, display.headerH() - 2, display.width(), 1); + display.fillRect(0, display.headerH() - display.sepH(), display.width(), display.sepH()); for (int i = 0; i < _visible && (_scroll + i) < SettingItem::Count; i++) { renderItem(display, _scroll + i, start_y + i * item_h); @@ -489,12 +497,14 @@ public: if (right && p->tx_power_dbm < 22) { p->tx_power_dbm++; _task->applyTxPower(); _dirty = true; return true; } if (left && p->tx_power_dbm > 2) { p->tx_power_dbm--; _task->applyTxPower(); _dirty = true; return true; } } +#if AUTO_OFF_MILLIS > 0 if (_selected == AUTO_OFF && p) { int idx = autoOffIndex(); if (right) idx = (idx + 1) % AUTO_OFF_COUNT; if (left) idx = (idx + AUTO_OFF_COUNT - 1) % AUTO_OFF_COUNT; if (left || right) { p->auto_off_secs = AUTO_OFF_OPTS[idx]; _dirty = true; return true; } } +#endif if (_selected == AUTO_LOCK && p && (left || right || enter)) { p->auto_lock ^= 1; _dirty = true; @@ -576,8 +586,10 @@ public: } }; +#if AUTO_OFF_MILLIS > 0 const uint16_t SettingsScreen::AUTO_OFF_OPTS[5] = { 5, 15, 30, 60, 0 }; const char* SettingsScreen::AUTO_OFF_LABELS[5] = { "5s", "15s", "30s", "60s", "never" }; +#endif #if ENV_INCLUDE_GPS == 1 const uint32_t SettingsScreen::GPS_INTERVAL_OPTS[6] = { 0, 30, 60, 300, 900, 1800 }; const char* SettingsScreen::GPS_INTERVAL_LABELS[6] = { "off", "30s", "1min", "5min", "15min", "30min" }; diff --git a/examples/companion_radio/ui-new/ToolsScreen.h b/examples/companion_radio/ui-new/ToolsScreen.h index 316ccc9b..cde36181 100644 --- a/examples/companion_radio/ui-new/ToolsScreen.h +++ b/examples/companion_radio/ui-new/ToolsScreen.h @@ -16,7 +16,7 @@ public: display.setTextSize(1); display.setColor(DisplayDriver::LIGHT); display.drawTextCentered(display.width() / 2, 0, "TOOLS"); - display.fillRect(0, display.headerH() - 1, display.width(), 1); + display.fillRect(0, display.headerH() - 1, display.width(), display.sepH()); int item_h = display.lineStep(); int start_y = display.listStart(); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 6d95f059..5fe4e73f 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -240,9 +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 + const int lh = display.getLineHeight(); + const int cw = display.getCharWidth(); + const int ind = cw + 2; // single-char indicator width + const int ind_gap = (lh >= 16) ? 3 : 1; // gap between indicator boxes int battLeftX; if (mode == 1) { // percent @@ -259,18 +260,19 @@ class HomeScreen : public UIScreen { display.print(buf); } else { // icon — scales with lh const int iconH = lh; - const int iconW = iconH * 3; + const int iconW = lh * 2; + const int bm = (lh >= 16) ? 3 : 2; // inner margin: 3px on landscape (2px border), 2px on OLED 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); + int fillW = (pct * (iconW - 2 * bm)) / 100; + display.fillRect(battLeftX + bm, bm, fillW, iconH - 2 * bm); } #ifdef PIN_BUZZER if (_task->isBuzzerQuiet()) { display.setColor(DisplayDriver::LIGHT); - int mx = battLeftX - ind - 1; + int mx = battLeftX - ind - ind_gap; display.fillRect(mx, 0, ind, lh); display.setColor(DisplayDriver::DARK); display.setCursor(mx + 1, 0); @@ -283,7 +285,7 @@ class HomeScreen : public UIScreen { // BT connection indicator (left of muted/battery icons) int leftmostX = battLeftX; if (_task->isSerialEnabled()) { - int btX = battLeftX - ind - 1; + int btX = battLeftX - ind - ind_gap; if (_task->hasConnection()) { display.setColor(DisplayDriver::LIGHT); display.fillRect(btX, 0, ind, lh); @@ -295,7 +297,7 @@ class HomeScreen : public UIScreen { display.setCursor(btX + 1, 0); display.print("b"); } - leftmostX = btX - 1; + leftmostX = btX - ind_gap; // "A" indicator — left of BT, blinks 50% duty at 4s period if (_node_prefs && _node_prefs->advert_auto_interval_sec > 0) { @@ -358,7 +360,7 @@ public: 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 + const int content_y = dots_y + 6; // first content row (6px gap keeps dots visible) // node name + battery — hidden on CLOCK page (full screen used for dashboard) if (_page != CLOCK) { @@ -385,8 +387,9 @@ public: int vi = 0; for (int i = 0; i < (int)Count; i++) { if (!isPageVisible(i)) continue; - if (vi == curr_vis) display.fillRect(x-1, dots_y-1, 3, 3); - else display.fillRect(x, dots_y, 1, 1); + int ds = (lh >= 16) ? 2 : 1; + if (vi == curr_vis) display.fillRect(x-ds, dots_y-ds, 2*ds+1, 2*ds+1); + else display.fillRect(x-ds+1, dots_y-ds+1, 2*ds-1, 2*ds-1); x += 10; vi++; } } @@ -431,8 +434,8 @@ public: display.drawTextCentered(display.width() / 2, date_y, buf); int sep_y = date_y + lh + 1; - int dash0 = sep_y + 3; - display.fillRect(0, sep_y, display.width(), 1); + int dash0 = sep_y + display.sepH() + 2; + display.fillRect(0, sep_y, display.width(), display.sepH()); // dashboard data fields if (_node_prefs) { @@ -1448,13 +1451,17 @@ void UITask::loop() { int delay_millis = curr->render(*_display); if (millis() < _alert_expiry && curr == home) { // render alert only on home screen _display->setTextSize(1); - int y = _display->height() / 3; - int p = _display->height() / 32; + int lh = _display->getLineHeight(); + int pad = 3; + int box_h = lh + pad * 2; + int box_w = _display->width() - 8; + int box_x = 4; + int box_y = (_display->height() - box_h) / 2; _display->setColor(DisplayDriver::DARK); - _display->fillRect(p, y, _display->width() - p*2, y); - _display->setColor(DisplayDriver::LIGHT); // draw box border - _display->drawRect(p, y, _display->width() - p*2, y); - _display->drawTextCentered(_display->width() / 2, y + p*3, _alert); + _display->fillRect(box_x, box_y, box_w, box_h); + _display->setColor(DisplayDriver::LIGHT); + _display->drawRect(box_x, box_y, box_w, box_h); + _display->drawTextCentered(_display->width() / 2, box_y + pad, _alert); _next_refresh = _alert_expiry; // will need refresh when alert is dismissed } else { _next_refresh = millis() + delay_millis; diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index c75baed9..c489f9cb 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -41,6 +41,8 @@ public: 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; } + // separator line thickness: 2px on landscape e-ink, 1px on OLED + int sepH() const { return (width() >= height()) ? 2 : 1; } virtual void drawTextCentered(int mid_x, int y, const char* str) { // helper method (override to optimise) int w = getTextWidth(str); setCursor(mid_x - w/2, y); @@ -135,7 +137,7 @@ public: case 0x00E7: return 'c'; case 0x00C7: return 'C'; // ç Ç case 0x00F1: return 'n'; case 0x00D1: return 'N'; // ñ Ñ case 0x00FD: return 'y'; case 0x00DD: return 'Y'; // ý Ý - default: return '\xDB'; // CP437 full block █ + default: return '?'; } } diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index ff4fff28..7d337a3a 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -139,6 +139,8 @@ void GxEPDDisplay::drawRect(int x, int y, int w, int h) { display_crc.update(w); display_crc.update(h); display.drawRect(x, y, w, h, _curr_color); + if (width() >= height() && w > 2 && h > 2) + display.drawRect(x + 1, y + 1, w - 2, h - 2, _curr_color); } void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {