mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 23:08:11 +00:00
Add "Full rfsh" setting (e-ink only) with options off/5/10/20/30 partial refreshes between full refreshes. Prevents ghosting on panels that accumulate artifacts after many partial updates. GxEPDDisplay counts partial refreshes and calls display.display(false) every N frames when the interval is set. Persisted in DataStore and applied at startup via applyFullRefreshInterval(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
234 lines
10 KiB
C++
234 lines
10 KiB
C++
#pragma once
|
||
|
||
#include <stdint.h>
|
||
#include <string.h>
|
||
|
||
class DisplayDriver {
|
||
int _w, _h;
|
||
protected:
|
||
bool _vw_dirty = true;
|
||
bool _vw_result = false;
|
||
DisplayDriver(int w, int h) { _w = w; _h = h; }
|
||
void setDimensions(int w, int h) { _w = w; _h = h; }
|
||
public:
|
||
enum Color { DARK=0, LIGHT, RED, GREEN, BLUE, YELLOW, ORANGE }; // on b/w screen, colors will be !=0 synonym of light
|
||
|
||
int width() const { return _w; }
|
||
int height() const { return _h; }
|
||
|
||
virtual bool isOn() = 0;
|
||
virtual void turnOn() = 0;
|
||
virtual void turnOff() = 0;
|
||
virtual void clear() = 0;
|
||
virtual void startFrame(Color bkg = DARK) = 0;
|
||
virtual void setTextSize(int sz) = 0;
|
||
virtual void setColor(Color c) = 0;
|
||
virtual void setCursor(int x, int y) = 0;
|
||
virtual void print(const char* str) = 0;
|
||
virtual void printWordWrap(const char* str, int max_width) { print(str); } // fallback to basic print() if no override
|
||
virtual void fillRect(int x, int y, int w, int h) = 0;
|
||
virtual void drawRect(int x, int y, int w, int h) = 0;
|
||
virtual void drawXbm(int x, int y, const uint8_t* bits, int w, int h) = 0;
|
||
virtual uint16_t getTextWidth(const char* str) = 0;
|
||
virtual int getCharWidth() const { return 6; } // typical character advance width (px)
|
||
virtual int getLineHeight() const { return 8; } // pixel rows per text line
|
||
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.
|
||
// Use these instead of hardcoded pixel values so layouts adapt to any display.
|
||
int lineStep() const { return getLineHeight() + 2; } // row pitch: text + gap
|
||
int headerH() const { return getLineHeight() + 3; } // title bar height
|
||
int listStart() const { return headerH(); } // y where list items begin
|
||
int listVisible(int itemH) const { return (height() - listStart()) / itemH; }
|
||
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; }
|
||
// true only on landscape e-ink; use instead of comparing pixel counts or getLineHeight()
|
||
#ifdef EINK_DISPLAY_MODEL
|
||
bool isLandscape() const { return width() >= height(); }
|
||
#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);
|
||
print(tmp);
|
||
}
|
||
virtual void drawTextRightAlign(int x_anch, int y, const char* str) {
|
||
char tmp[256]; translateUTF8ToBlocks(tmp, str, sizeof(tmp));
|
||
int w = getTextWidth(tmp);
|
||
setCursor(x_anch - w, y);
|
||
print(tmp);
|
||
}
|
||
virtual void drawTextLeftAlign(int x_anch, int y, const char* str) {
|
||
char tmp[256]; translateUTF8ToBlocks(tmp, str, sizeof(tmp));
|
||
setCursor(x_anch, y);
|
||
print(tmp);
|
||
}
|
||
|
||
static char transliterateCodepoint(uint32_t cp) {
|
||
switch (cp) {
|
||
// Polish
|
||
case 0x0105: return 'a'; case 0x0104: return 'A'; // ą Ą
|
||
case 0x0107: return 'c'; case 0x0106: return 'C'; // ć Ć
|
||
case 0x0119: return 'e'; case 0x0118: return 'E'; // ę Ę
|
||
case 0x0142: return 'l'; case 0x0141: return 'L'; // ł Ł
|
||
case 0x0144: return 'n'; case 0x0143: return 'N'; // ń Ń
|
||
case 0x00F3: return 'o'; case 0x00D3: return 'O'; // ó Ó
|
||
case 0x015B: return 's'; case 0x015A: return 'S'; // ś Ś
|
||
case 0x017A: return 'z'; case 0x0179: return 'Z'; // ź Ź
|
||
case 0x017C: return 'z'; case 0x017B: return 'Z'; // ż Ż
|
||
// Czech/Slovak
|
||
case 0x010D: return 'c'; case 0x010C: return 'C'; // č Č
|
||
case 0x0161: return 's'; case 0x0160: return 'S'; // š Š
|
||
case 0x017E: return 'z'; case 0x017D: return 'Z'; // ž Ž
|
||
case 0x0159: return 'r'; case 0x0158: return 'R'; // ř Ř
|
||
case 0x011B: return 'e'; case 0x011A: return 'E'; // ě Ě
|
||
case 0x016F: return 'u'; case 0x016E: return 'U'; // ů Ů
|
||
case 0x010F: return 'd'; case 0x010E: return 'D'; // ď Ď
|
||
case 0x0165: return 't'; case 0x0164: return 'T'; // ť Ť
|
||
case 0x0148: return 'n'; case 0x0147: return 'N'; // ň Ň
|
||
case 0x013E: return 'l'; case 0x013D: return 'L'; // ľ Ľ
|
||
case 0x013A: return 'l'; case 0x0139: return 'L'; // ĺ Ĺ
|
||
case 0x0155: return 'r'; case 0x0154: return 'R'; // ŕ Ŕ
|
||
// German
|
||
case 0x00E4: return 'a'; case 0x00C4: return 'A'; // ä Ä
|
||
case 0x00F6: return 'o'; case 0x00D6: return 'O'; // ö Ö
|
||
case 0x00FC: return 'u'; case 0x00DC: return 'U'; // ü Ü
|
||
case 0x00DF: return 's'; // ß
|
||
// Scandinavian
|
||
case 0x00E5: return 'a'; case 0x00C5: return 'A'; // å Å
|
||
case 0x00F8: return 'o'; case 0x00D8: return 'O'; // ø Ø
|
||
case 0x00E6: return 'a'; case 0x00C6: return 'A'; // æ Æ
|
||
// Hungarian
|
||
case 0x0151: return 'o'; case 0x0150: return 'O'; // ő Ő
|
||
case 0x0171: return 'u'; case 0x0170: return 'U'; // ű Ű
|
||
// Romanian
|
||
case 0x0103: return 'a'; case 0x0102: return 'A'; // ă Ă
|
||
case 0x0219: return 's'; case 0x0218: return 'S'; // ș Ș
|
||
case 0x021B: return 't'; case 0x021A: return 'T'; // ț Ț
|
||
// Croatian
|
||
case 0x0111: return 'd'; case 0x0110: return 'D'; // đ Đ
|
||
// Icelandic
|
||
case 0x00F0: return 'd'; case 0x00D0: return 'D'; // ð Ð (eth)
|
||
case 0x00FE: return 't'; case 0x00DE: return 'T'; // þ Þ (thorn)
|
||
// Romanian cedilla variants (legacy encoding)
|
||
case 0x0163: return 't'; case 0x0162: return 'T'; // ţ Ţ
|
||
// Turkish
|
||
case 0x011F: return 'g'; case 0x011E: return 'G'; // ğ Ğ
|
||
case 0x015F: return 's'; case 0x015E: return 'S'; // ş Ş
|
||
case 0x0131: return 'i'; // ı
|
||
// Baltic (Lithuanian/Latvian)
|
||
case 0x0101: return 'a'; case 0x0100: return 'A'; // ā Ā
|
||
case 0x0113: return 'e'; case 0x0112: return 'E'; // ē Ē
|
||
case 0x012B: return 'i'; case 0x012A: return 'I'; // ī Ī
|
||
case 0x016B: return 'u'; case 0x016A: return 'U'; // ū Ū
|
||
case 0x0123: return 'g'; case 0x0122: return 'G'; // ģ Ģ
|
||
case 0x0137: return 'k'; case 0x0136: return 'K'; // ķ Ķ
|
||
case 0x013C: return 'l'; case 0x013B: return 'L'; // ļ Ļ
|
||
case 0x0146: return 'n'; case 0x0145: return 'N'; // ņ Ņ
|
||
case 0x0157: return 'r'; case 0x0156: return 'R'; // ŗ Ŗ
|
||
case 0x0173: return 'u'; case 0x0172: return 'U'; // ų Ų
|
||
case 0x0117: return 'e'; case 0x0116: return 'E'; // ė Ė
|
||
case 0x012F: return 'i'; case 0x012E: return 'I'; // į Į
|
||
// French/Spanish/Portuguese common accents
|
||
case 0x00E0: case 0x00E1: case 0x00E2: case 0x00E3: return 'a';
|
||
case 0x00C0: case 0x00C1: case 0x00C2: case 0x00C3: return 'A';
|
||
case 0x00E8: case 0x00E9: case 0x00EA: case 0x00EB: return 'e';
|
||
case 0x00C8: case 0x00C9: case 0x00CA: case 0x00CB: return 'E';
|
||
case 0x00EC: case 0x00ED: case 0x00EE: case 0x00EF: return 'i';
|
||
case 0x00CC: case 0x00CD: case 0x00CE: case 0x00CF: return 'I';
|
||
case 0x00F2: case 0x00F4: case 0x00F5: return 'o';
|
||
case 0x00D2: case 0x00D4: case 0x00D5: return 'O';
|
||
case 0x00F9: case 0x00FA: case 0x00FB: return 'u';
|
||
case 0x00D9: case 0x00DA: case 0x00DB: return 'U';
|
||
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';
|
||
}
|
||
}
|
||
|
||
// convert UTF-8 to ASCII, transliterating accented/diacritic characters (callable without a display instance)
|
||
static void translateUTF8Static(char* dest, const char* src, size_t dest_size) {
|
||
size_t j = 0;
|
||
const uint8_t* p = (const uint8_t*)src;
|
||
while (*p && j < dest_size - 1) {
|
||
uint8_t c = *p++;
|
||
if (c < 0x80) {
|
||
if (c >= 32) dest[j++] = c;
|
||
} else {
|
||
uint32_t cp = c;
|
||
if ((c & 0xE0) == 0xC0) {
|
||
cp = c & 0x1F;
|
||
if (*p) cp = (cp << 6) | (*p++ & 0x3F);
|
||
} else if ((c & 0xF0) == 0xE0) {
|
||
cp = c & 0x0F;
|
||
if (*p) cp = (cp << 6) | (*p++ & 0x3F);
|
||
if (*p) cp = (cp << 6) | (*p++ & 0x3F);
|
||
} else {
|
||
while (*p && (*p & 0xC0) == 0x80) p++;
|
||
dest[j++] = '\xDB';
|
||
continue;
|
||
}
|
||
dest[j++] = transliterateCodepoint(cp);
|
||
}
|
||
}
|
||
dest[j] = 0;
|
||
}
|
||
|
||
virtual void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) {
|
||
translateUTF8Static(dest, src, dest_size);
|
||
}
|
||
|
||
// draw text with ellipsis if it exceeds max_width
|
||
virtual void drawTextEllipsized(int x, int y, int max_width, const char* str) {
|
||
char temp_str[256]; // reasonable buffer size
|
||
translateUTF8ToBlocks(temp_str, str, sizeof(temp_str));
|
||
|
||
if (getTextWidth(temp_str) <= max_width) {
|
||
setCursor(x, y);
|
||
print(temp_str);
|
||
return;
|
||
}
|
||
|
||
// for variable-width fonts (GxEPD), add space after ellipsis
|
||
// for fixed-width fonts (OLED), keep tight spacing to save precious characters
|
||
const char* ellipsis;
|
||
// use a simple heuristic: if 'i' and 'l' have different widths, it's variable-width
|
||
if (_vw_dirty) {
|
||
_vw_result = (getTextWidth("i") != getTextWidth("l"));
|
||
_vw_dirty = false;
|
||
}
|
||
if (_vw_result) {
|
||
ellipsis = "... "; // variable-width fonts: add space
|
||
} else {
|
||
ellipsis = "..."; // fixed-width fonts: no space
|
||
}
|
||
|
||
int ellipsis_width = getTextWidth(ellipsis);
|
||
int str_len = strlen(temp_str);
|
||
|
||
while (str_len > 0 && getTextWidth(temp_str) > max_width - ellipsis_width) {
|
||
temp_str[--str_len] = 0;
|
||
}
|
||
// Strip orphaned UTF-8 leading byte left by byte-at-a-time trimming above.
|
||
while (str_len > 0 && ((uint8_t)temp_str[str_len - 1] & 0xC0) == 0xC0) {
|
||
temp_str[--str_len] = 0;
|
||
}
|
||
strcat(temp_str, ellipsis);
|
||
|
||
setCursor(x, y);
|
||
print(temp_str);
|
||
}
|
||
|
||
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 endFrame() = 0;
|
||
};
|