fix(ui): unified unknown-char block rendering and display layout fixes

- GxEPDDisplay::print() renders '\xDB' as filled rect for Lemon font,
  matching OLED Lemon block behavior
- Remove translateUTF8ToBlocks override (base class passthrough sufficient)
- transliterateCodepoint fallback changed '?' → '\xDB' for consistency
- drawTextCentered/Right/LeftAlign now call translateUTF8ToBlocks
- isLemonFont() added to DisplayDriver, SH1106Display, GxEPDDisplay
- isLandscape() guarded by EINK_DISPLAY_MODEL to distinguish OLED vs e-ink
- ind_h trims indicator box height only for Lemon font
- getTextWidth disables wrap before getTextBounds, restores after
- QuickMsgScreen: tighter hist_item padding; 2 messages visible with Lemon
- Hibernate hint fits on one line when width allows, splits otherwise
- BLE PIN page shows toggle hint on landscape e-ink
- Remove unused muted_icon bitmap

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-05-23 14:45:24 +02:00
co-authored by Claude Sonnet 4.6
parent 6cdd9d01d4
commit 85cfb6f1af
7 changed files with 90 additions and 48 deletions
@@ -87,8 +87,9 @@ class QuickMsgScreen : public UIScreen {
static const char* skipReplyPrefix(const char* text) { static const char* skipReplyPrefix(const char* text) {
if (text[0] == '@' && text[1] == '[') { if (text[0] == '@' && text[1] == '[') {
const char* close = strchr(text + 2, ']'); const char* close = strchr(text + 2, ']');
if (close && close[1] == ' ' && close[2]) return close + 2; if (close && close[1] == ' ' && close[2]) text = close + 2;
} }
while (*text == '\n' || *text == '\r' || *text == ' ') text++;
return text; return text;
} }
@@ -630,13 +631,12 @@ public:
return 500; return 500;
} }
int hist_box_h = 2 * lh + 3; int hist_box_h = 2 * lh + 1;
int hist_item_h = hist_box_h + 2; int hist_item_h = hist_box_h + 1;
int hist_start_y = display.headerH(); int hist_start_y = display.headerH();
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 = display.height() - lh - 2; int cby = display.height() - lh - 2;
_hist_visible = (cby - 1 - hist_start_y) / hist_item_h;
if (_hist_visible < 1) _hist_visible = 1;
char title[24]; char title[24];
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
@@ -666,7 +666,7 @@ public:
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender); display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender);
if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); } if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); }
display.drawTextEllipsized(3, y + lh + 2, display.width() - cw - 2, skipReplyPrefix(e.text)); display.drawTextEllipsized(3, y + lh + 1, display.width() - cw - 2, skipReplyPrefix(e.text));
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, y, display.width(), hist_box_h); display.drawRect(0, y, display.width(), hist_box_h);
@@ -675,7 +675,7 @@ public:
display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender); display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender);
if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); } if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); }
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawTextEllipsized(3, y + lh + 2, display.width() - cw - 2, skipReplyPrefix(e.text)); display.drawTextEllipsized(3, y + lh + 1, display.width() - cw - 2, skipReplyPrefix(e.text));
} }
} }
@@ -733,13 +733,12 @@ public:
return 2000; return 2000;
} }
int hist_box_h = 2 * lh + 3; int hist_box_h = 2 * lh + 1;
int hist_item_h = hist_box_h + 2; int hist_item_h = hist_box_h + 1;
int hist_start_y = display.headerH(); int hist_start_y = display.headerH();
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 = display.height() - lh - 2; int cby = display.height() - lh - 2;
_hist_visible = (cby - 1 - hist_start_y) / hist_item_h;
if (_hist_visible < 1) _hist_visible = 1;
ChannelDetails ch; ChannelDetails ch;
the_mesh.getChannel(_sel_channel_idx, ch); the_mesh.getChannel(_sel_channel_idx, ch);
@@ -781,7 +780,7 @@ public:
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender); display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender);
if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); } if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); }
display.drawTextEllipsized(3, y + lh + 2, display.width() - cw - 2, skipReplyPrefix(msg_part)); display.drawTextEllipsized(3, y + lh + 1, display.width() - cw - 2, skipReplyPrefix(msg_part));
} else { } else {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, y, display.width(), hist_box_h); display.drawRect(0, y, display.width(), hist_box_h);
@@ -790,7 +789,7 @@ public:
display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender); display.drawTextEllipsized(3, y + 1, display.width() - cw - 2 - age_w, sender);
if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); } if (age[0]) { display.setCursor(display.width() - 3 - display.getTextWidth(age), y + 1); display.print(age); }
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawTextEllipsized(3, y + lh + 2, display.width() - cw - 2, skipReplyPrefix(msg_part)); display.drawTextEllipsized(3, y + lh + 1, display.width() - cw - 2, skipReplyPrefix(msg_part));
} }
} }
+37 -18
View File
@@ -291,7 +291,8 @@ class HomeScreen : public UIScreen {
const int lh = display.getLineHeight(); const int lh = display.getLineHeight();
const int cw = display.getCharWidth(); const int cw = display.getCharWidth();
const int ind = cw + 2; // single-char indicator width const int ind = cw + 2; // single-char indicator width
const int ind_gap = (lh >= 16) ? 3 : 1; // gap between indicator boxes const int ind_h = display.isLemonFont() ? lh - 2 : lh;
const int ind_gap = display.isLandscape() ? 3 : 1; // gap between indicator boxes
int battLeftX; int battLeftX;
if (mode == 1) { // percent if (mode == 1) { // percent
@@ -309,7 +310,7 @@ class HomeScreen : public UIScreen {
} else { // icon — scales with lh } else { // icon — scales with lh
const int iconH = lh; const int iconH = lh;
const int iconW = lh * 2; const int iconW = lh * 2;
const int bm = (lh >= 16) ? 3 : 2; // inner margin: 3px on landscape (2px border), 2px on OLED const int bm = display.isLandscape() ? 3 : 2; // inner margin: 3px on landscape e-ink, 2px on OLED/portrait
battLeftX = display.width() - iconW - 3; battLeftX = display.width() - iconW - 3;
display.drawRect(battLeftX, 0, iconW, iconH); display.drawRect(battLeftX, 0, iconW, iconH);
display.fillRect(battLeftX + iconW, iconH / 4, 2, iconH / 2); display.fillRect(battLeftX + iconW, iconH / 4, 2, iconH / 2);
@@ -321,7 +322,7 @@ class HomeScreen : public UIScreen {
if (_task->isBuzzerQuiet()) { if (_task->isBuzzerQuiet()) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
int mx = battLeftX - ind - ind_gap; int mx = battLeftX - ind - ind_gap;
display.fillRect(mx, 0, ind, lh); display.fillRect(mx, 0, ind, ind_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.setCursor(mx + 1, 0); display.setCursor(mx + 1, 0);
display.print("M"); display.print("M");
@@ -336,7 +337,7 @@ class HomeScreen : public UIScreen {
int btX = battLeftX - ind - ind_gap; int btX = battLeftX - ind - ind_gap;
if (_task->hasConnection()) { if (_task->hasConnection()) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(btX, 0, ind, lh); display.fillRect(btX, 0, ind, ind_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.setCursor(btX + 1, 0); display.setCursor(btX + 1, 0);
display.print("B"); display.print("B");
@@ -357,7 +358,7 @@ class HomeScreen : public UIScreen {
#endif #endif
if (show_a) { if (show_a) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(aX, 0, ind, lh); display.fillRect(aX, 0, ind, ind_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.setCursor(aX + 1, 0); display.setCursor(aX + 1, 0);
display.print("A"); display.print("A");
@@ -435,7 +436,7 @@ public:
for (int i = 0; i < n; i++) if (order[i] == _page) { curr_vis = i; break; } for (int i = 0; i < n; i++) if (order[i] == _page) { curr_vis = i; break; }
int x = display.width() / 2 - 5 * (n - 1); int x = display.width() / 2 - 5 * (n - 1);
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
int ds = (lh >= 16) ? 2 : 1; int ds = display.isLandscape() ? 2 : 1;
if (i == curr_vis) display.fillRect(x-ds, dots_y-ds, 2*ds+1, 2*ds+1); if (i == 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); else display.fillRect(x-ds+1, dots_y-ds+1, 2*ds-1, 2*ds-1);
x += 10; x += 10;
@@ -460,7 +461,11 @@ public:
char buf[24]; char buf[24];
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.setTextSize(2); display.setTextSize(2);
#ifdef EINK_DISPLAY_MODEL
bool show_sec = false;
#else
bool show_sec = !_node_prefs || !_node_prefs->clock_hide_seconds; bool show_sec = !_node_prefs || !_node_prefs->clock_hide_seconds;
#endif
bool h12 = _node_prefs && _node_prefs->clock_12h; bool h12 = _node_prefs && _node_prefs->clock_12h;
if (h12) { if (h12) {
int h = ti->tm_hour % 12; if (h == 0) h = 12; int h = ti->tm_hour % 12; if (h == 0) h = 12;
@@ -609,19 +614,23 @@ public:
display.print(tmp); display.print(tmp);
} else if (_page == HomePage::BLUETOOTH) { } else if (_page == HomePage::BLUETOOTH) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
int icon_y = content_y;
display.drawXbm((display.width() - 32) / 2, icon_y,
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off,
32, 32);
display.setTextSize(1); display.setTextSize(1);
int pin_y = icon_y + 32 + 3; display.drawXbm((display.width() - 32) / 2, content_y,
int hint_y = pin_y + step; _task->isSerialEnabled() ? bluetooth_on : bluetooth_off, 32, 32);
if (_task->isSerialEnabled() && !_task->hasConnection() && the_mesh.getBLEPin() != 0) { const int text_y = content_y + 32 + 3;
const bool waiting_for_pair = _task->isSerialEnabled() && !_task->hasConnection() && the_mesh.getBLEPin() != 0;
if (waiting_for_pair && !display.isLandscape()) {
char pin_buf[16]; char pin_buf[16];
snprintf(pin_buf, sizeof(pin_buf), "PIN: %d", the_mesh.getBLEPin()); snprintf(pin_buf, sizeof(pin_buf), "PIN: %d", the_mesh.getBLEPin());
display.drawTextCentered(display.width() / 2, pin_y, pin_buf); display.drawTextCentered(display.width() / 2, text_y, pin_buf);
} else if (waiting_for_pair) {
char pin_buf[16];
snprintf(pin_buf, sizeof(pin_buf), "PIN: %d", the_mesh.getBLEPin());
display.drawTextCentered(display.width() / 2, text_y, pin_buf);
display.drawTextCentered(display.width() / 2, text_y + step, "toggle: " PRESS_LABEL);
} else {
display.drawTextCentered(display.width() / 2, text_y, "toggle: " PRESS_LABEL);
} }
display.drawTextCentered(display.width() / 2, hint_y, "toggle: " PRESS_LABEL);
} else if (_page == HomePage::ADVERT) { } else if (_page == HomePage::ADVERT) {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.drawXbm((display.width() - 32) / 2, content_y, advert_icon, 32, 32); display.drawXbm((display.width() - 32) / 2, content_y, advert_icon, 32, 32);
@@ -766,9 +775,19 @@ public:
display.drawTextCentered(display.width() / 2, content_y + step, "hibernating..."); display.drawTextCentered(display.width() / 2, content_y + step, "hibernating...");
} else { } else {
display.drawXbm((display.width() - 32) / 2, content_y, power_icon, 32, 32); display.drawXbm((display.width() - 32) / 2, content_y, power_icon, 32, 32);
int hint_base = content_y + 32 + 3; const int text_y = content_y + 32 + 3;
display.drawTextCentered(display.width() / 2, hint_base, "hibernate:"); const int lh1 = display.getLineHeight();
display.drawTextCentered(display.width() / 2, hint_base + step, PRESS_LABEL); if (text_y + lh1 <= display.height()) {
char hib_hint[32];
snprintf(hib_hint, sizeof(hib_hint), "hibernate: %s", PRESS_LABEL);
if (display.getTextWidth(hib_hint) < display.width()) {
display.drawTextCentered(display.width() / 2, text_y, hib_hint);
} else {
display.drawTextCentered(display.width() / 2, text_y, "hibernate:");
if (text_y + step + lh1 <= display.height())
display.drawTextCentered(display.width() / 2, text_y + step, PRESS_LABEL);
}
}
} }
} }
bool auto_adv = _node_prefs && _node_prefs->advert_auto_interval_sec > 0; bool auto_adv = _node_prefs && _node_prefs->advert_auto_interval_sec > 0;
-4
View File
@@ -116,7 +116,3 @@ static const uint8_t advert_icon[] = {
0x04, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}; };
static const uint8_t muted_icon[] = {
0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20
};
+19 -9
View File
@@ -31,6 +31,7 @@ public:
virtual int getCharWidth() const { return 6; } // typical character advance width (px) virtual int getCharWidth() const { return 6; } // typical character advance width (px)
virtual int getLineHeight() const { return 8; } // pixel rows per text line virtual int getLineHeight() const { return 8; } // pixel rows per text line
virtual void setLemonFont(bool) { } // no-op; overridden by displays that support Lemon virtual void setLemonFont(bool) { } // no-op; overridden by displays that support Lemon
virtual bool isLemonFont() const { return false; }
// Layout helpers — derived from font metrics and screen size. // Layout helpers — derived from font metrics and screen size.
// Use these instead of hardcoded pixel values so layouts adapt to any display. // Use these instead of hardcoded pixel values so layouts adapt to any display.
@@ -41,21 +42,30 @@ public:
int listVisible() const { return listVisible(lineStep()); } int listVisible() const { return listVisible(lineStep()); }
// x where a right-side value column starts (leaves ~8 chars for the value) // x where a right-side value column starts (leaves ~8 chars for the value)
int valCol() const { return width() - getCharWidth() * 8; } int valCol() const { return width() - getCharWidth() * 8; }
// separator line thickness: 2px on landscape e-ink, 1px on OLED // true only on landscape e-ink; use instead of comparing pixel counts or getLineHeight()
int sepH() const { return (width() >= height()) ? 2 : 1; } #ifdef EINK_DISPLAY_MODEL
virtual void drawTextCentered(int mid_x, int y, const char* str) { // helper method (override to optimise) bool isLandscape() const { return width() >= height(); }
int w = getTextWidth(str); #else
bool isLandscape() const { return false; }
#endif
// separator line thickness: 2px on landscape e-ink, 1px everywhere else
int sepH() const { return isLandscape() ? 2 : 1; }
virtual void drawTextCentered(int mid_x, int y, const char* str) {
char tmp[256]; translateUTF8ToBlocks(tmp, str, sizeof(tmp));
int w = getTextWidth(tmp);
setCursor(mid_x - w/2, y); setCursor(mid_x - w/2, y);
print(str); print(tmp);
} }
virtual void drawTextRightAlign(int x_anch, int y, const char* str) { virtual void drawTextRightAlign(int x_anch, int y, const char* str) {
int w = getTextWidth(str); char tmp[256]; translateUTF8ToBlocks(tmp, str, sizeof(tmp));
int w = getTextWidth(tmp);
setCursor(x_anch - w, y); setCursor(x_anch - w, y);
print(str); print(tmp);
} }
virtual void drawTextLeftAlign(int x_anch, int y, const char* str) { virtual void drawTextLeftAlign(int x_anch, int y, const char* str) {
char tmp[256]; translateUTF8ToBlocks(tmp, str, sizeof(tmp));
setCursor(x_anch, y); setCursor(x_anch, y);
print(str); print(tmp);
} }
static char transliterateCodepoint(uint32_t cp) { static char transliterateCodepoint(uint32_t cp) {
@@ -137,7 +147,7 @@ public:
case 0x00E7: return 'c'; case 0x00C7: return 'C'; // ç Ç case 0x00E7: return 'c'; case 0x00C7: return 'C'; // ç Ç
case 0x00F1: return 'n'; case 0x00D1: return 'N'; // ñ Ñ case 0x00F1: return 'n'; case 0x00D1: return 'N'; // ñ Ñ
case 0x00FD: return 'y'; case 0x00DD: return 'Y'; // ý Ý case 0x00FD: return 'y'; case 0x00DD: return 'Y'; // ý Ý
default: return '?'; default: return '\xDB';
} }
} }
+18 -1
View File
@@ -122,7 +122,22 @@ void GxEPDDisplay::setCursor(int x, int y) {
void GxEPDDisplay::print(const char* str) { void GxEPDDisplay::print(const char* str) {
display_crc.update<char>(str, strlen(str)); display_crc.update<char>(str, strlen(str));
display.print(str); if (!_use_lemon) {
display.print(str);
return;
}
int sc = (width() >= height()) ? 2 : 1;
for (const char* p = str; *p; p++) {
if ((uint8_t)*p == 0xDB) {
int cx = display.getCursorX();
int cy = display.getCursorY();
display.fillRect(cx, cy - 8 * sc, 5 * sc, 8 * sc, _curr_color);
display.setCursor(cx + 6 * sc, cy);
} else {
char tmp[2] = {*p, 0};
display.print(tmp);
}
}
} }
void GxEPDDisplay::fillRect(int x, int y, int w, int h) { void GxEPDDisplay::fillRect(int x, int y, int w, int h) {
@@ -162,9 +177,11 @@ void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
} }
uint16_t GxEPDDisplay::getTextWidth(const char* str) { uint16_t GxEPDDisplay::getTextWidth(const char* str) {
display.setTextWrap(false);
int16_t x1, y1; int16_t x1, y1;
uint16_t w, h; uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
display.setTextWrap(true);
return w; return w;
} }
+1 -1
View File
@@ -83,7 +83,7 @@ public:
return (_use_lemon ? 10 : 8) * sc; return (_use_lemon ? 10 : 8) * sc;
} }
void setLemonFont(bool enabled) override { _use_lemon = enabled; } void setLemonFont(bool enabled) override { _use_lemon = enabled; }
bool isLemonFont() const override { return _use_lemon; }
bool begin(); bool begin();
bool isOn() override { return _isOn; } bool isOn() override { return _isOn; }
+1
View File
@@ -51,6 +51,7 @@ public:
int getCharWidth() const override { return (_use_lemon ? 5 : 6) * _text_sz; } int getCharWidth() const override { return (_use_lemon ? 5 : 6) * _text_sz; }
int getLineHeight() const override { return (_use_lemon ? 9 : 8) * _text_sz; } int getLineHeight() const override { return (_use_lemon ? 9 : 8) * _text_sz; }
void setLemonFont(bool enabled) override { _use_lemon = enabled; } void setLemonFont(bool enabled) override { _use_lemon = enabled; }
bool isLemonFont() const override { return _use_lemon; }
void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override; void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override;
void setBrightness(uint8_t level) override; void setBrightness(uint8_t level) override;
void endFrame() override; void endFrame() override;