mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-07 17:22:44 +02:00
Fix to beer emoji mug handle; font changes that support diacritics meaning updates to display drivers, fonts, highlighting, sprites; build flag added to both tdpro and t5s3 pro to enable diacritics rendering changes
This commit is contained in:
@@ -22,7 +22,90 @@
|
||||
// CLEAR_FAST, CLEAR_SLOW — full refresh modes
|
||||
|
||||
// Periodic slow (deep) refresh to clear ghosting
|
||||
#define FULL_SLOW_PERIOD 1 // every frame — eliminates ghosting (increase to 2+ for less flashing)
|
||||
#define FULL_SLOW_PERIOD 1 // every frame -- eliminates ghosting (increase to 2+ for less flashing)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UTF-8 helpers for diacritic / extended Latin rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static uint32_t utf8Decode(const uint8_t* s, int len, int* consumed) {
|
||||
if (len <= 0) { *consumed = 0; return 0; }
|
||||
uint8_t b = s[0];
|
||||
if (b < 0x80) { *consumed = 1; return b; }
|
||||
if ((b & 0xE0) == 0xC0 && len >= 2 && (s[1] & 0xC0) == 0x80) {
|
||||
*consumed = 2;
|
||||
return ((uint32_t)(b & 0x1F) << 6) | (s[1] & 0x3F);
|
||||
}
|
||||
if ((b & 0xF0) == 0xE0 && len >= 3 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80) {
|
||||
*consumed = 3;
|
||||
return ((uint32_t)(b & 0x0F) << 12) | ((uint32_t)(s[1] & 0x3F) << 6) | (s[2] & 0x3F);
|
||||
}
|
||||
if ((b & 0xF8) == 0xF0 && len >= 4 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80 && (s[3] & 0xC0) == 0x80) {
|
||||
*consumed = 4;
|
||||
return ((uint32_t)(b & 0x07) << 18) | ((uint32_t)(s[1] & 0x3F) << 12) | ((uint32_t)(s[2] & 0x3F) << 6) | (s[3] & 0x3F);
|
||||
}
|
||||
*consumed = 1;
|
||||
return 0xFFFD;
|
||||
}
|
||||
|
||||
static char foldToAscii(uint32_t cp) {
|
||||
if (cp < 0x80) return (char)cp;
|
||||
if (cp >= 0xC0 && cp <= 0xFF) {
|
||||
static const char f[64] = {
|
||||
'A','A','A','A','A','A','A','C','E','E','E','E','I','I','I','I',
|
||||
'D','N','O','O','O','O','O', 0 ,'O','U','U','U','U','Y', 0 ,'s',
|
||||
'a','a','a','a','a','a','a','c','e','e','e','e','i','i','i','i',
|
||||
'd','n','o','o','o','o','o', 0 ,'o','u','u','u','u','y', 0 ,'y'
|
||||
};
|
||||
return f[cp - 0xC0];
|
||||
}
|
||||
if (cp >= 0x100 && cp <= 0x17F) {
|
||||
switch (cp) {
|
||||
case 0x100: case 0x102: case 0x104: return 'A';
|
||||
case 0x101: case 0x103: case 0x105: return 'a';
|
||||
case 0x106: case 0x108: case 0x10A: case 0x10C: return 'C';
|
||||
case 0x107: case 0x109: case 0x10B: case 0x10D: return 'c';
|
||||
case 0x10E: case 0x110: return 'D';
|
||||
case 0x10F: case 0x111: return 'd';
|
||||
case 0x112: case 0x114: case 0x116: case 0x118: case 0x11A: return 'E';
|
||||
case 0x113: case 0x115: case 0x117: case 0x119: case 0x11B: return 'e';
|
||||
case 0x11C: case 0x11E: case 0x120: case 0x122: return 'G';
|
||||
case 0x11D: case 0x11F: case 0x121: case 0x123: return 'g';
|
||||
case 0x124: case 0x126: return 'H'; case 0x125: case 0x127: return 'h';
|
||||
case 0x128: case 0x12A: case 0x12C: case 0x12E: case 0x130: return 'I';
|
||||
case 0x129: case 0x12B: case 0x12D: case 0x12F: case 0x131: return 'i';
|
||||
case 0x134: return 'J'; case 0x135: return 'j';
|
||||
case 0x136: return 'K'; case 0x137: case 0x138: return 'k';
|
||||
case 0x139: case 0x13B: case 0x13D: case 0x13F: case 0x141: return 'L';
|
||||
case 0x13A: case 0x13C: case 0x13E: case 0x140: case 0x142: return 'l';
|
||||
case 0x143: case 0x145: case 0x147: return 'N';
|
||||
case 0x144: case 0x146: case 0x148: case 0x149: return 'n';
|
||||
case 0x14C: case 0x14E: case 0x150: return 'O';
|
||||
case 0x14D: case 0x14F: case 0x151: return 'o';
|
||||
case 0x152: return 'O'; case 0x153: return 'o';
|
||||
case 0x154: case 0x156: case 0x158: return 'R';
|
||||
case 0x155: case 0x157: case 0x159: return 'r';
|
||||
case 0x15A: case 0x15C: case 0x15E: case 0x160: return 'S';
|
||||
case 0x15B: case 0x15D: case 0x15F: case 0x161: return 's';
|
||||
case 0x162: case 0x164: case 0x166: return 'T';
|
||||
case 0x163: case 0x165: case 0x167: return 't';
|
||||
case 0x168: case 0x16A: case 0x16C: case 0x16E: case 0x170: case 0x172: return 'U';
|
||||
case 0x169: case 0x16B: case 0x16D: case 0x16F: case 0x171: case 0x173: return 'u';
|
||||
case 0x174: return 'W'; case 0x175: return 'w';
|
||||
case 0x176: case 0x178: return 'Y'; case 0x177: return 'y';
|
||||
case 0x179: case 0x17B: case 0x17D: return 'Z';
|
||||
case 0x17A: case 0x17C: case 0x17E: return 'z';
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool hasNonAscii(const char* str) {
|
||||
for (int i = 0; str[i]; i++) {
|
||||
if ((uint8_t)str[i] >= 0x80) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FastEPDDisplay::~FastEPDDisplay() {
|
||||
delete _canvas;
|
||||
@@ -128,43 +211,55 @@ void FastEPDDisplay::setTextSize(int sz) {
|
||||
const GFXfont* customFont = meckGetFont(_fontStyle, sz);
|
||||
if (customFont) {
|
||||
_canvas->setFont(customFont);
|
||||
// textSize 5 (clock face) uses ×5 scaling even with custom fonts
|
||||
_canvas->setTextSize(sz == 5 ? 5 : 1);
|
||||
_currentFont = customFont;
|
||||
// textSize 5 (clock face) uses x5 scaling even with custom fonts
|
||||
_currentTextScale = (sz == 5) ? 5 : 1;
|
||||
_canvas->setTextSize(_currentTextScale);
|
||||
return;
|
||||
}
|
||||
|
||||
// Classic style (or fallback) — original FreeSans/FreeSerif fonts
|
||||
// Classic style (or fallback) -- original FreeSans/FreeSerif fonts
|
||||
// Toggle between font families via -D MECK_SERIF_FONT build flag
|
||||
_currentTextScale = 1;
|
||||
switch(sz) {
|
||||
case 0: // Body text — reader content, settings rows, messages, footers
|
||||
case 0: // Body text -- reader content, settings rows, messages, footers
|
||||
#ifdef MECK_SERIF_FONT
|
||||
_canvas->setFont(&FreeSerif12pt7b);
|
||||
_currentFont = &FreeSerif12pt7b;
|
||||
#else
|
||||
_canvas->setFont(&FreeSans12pt7b);
|
||||
_currentFont = &FreeSans12pt7b;
|
||||
#endif
|
||||
_canvas->setTextSize(1);
|
||||
break;
|
||||
case 1: // Headings — screen titles, channel names (bold, same height as body)
|
||||
case 1: // Headings -- screen titles, channel names (bold, same height as body)
|
||||
_canvas->setFont(&FreeSansBold12pt7b);
|
||||
_currentFont = &FreeSansBold12pt7b;
|
||||
_canvas->setTextSize(1);
|
||||
break;
|
||||
case 2: // Large bold — MSG count, tile letters
|
||||
case 2: // Large bold -- MSG count, tile letters
|
||||
_canvas->setFont(&FreeSansBold18pt7b);
|
||||
_currentFont = &FreeSansBold18pt7b;
|
||||
_canvas->setTextSize(1);
|
||||
break;
|
||||
case 3: // Extra large — splash screen title
|
||||
case 3: // Extra large -- splash screen title
|
||||
_canvas->setFont(&FreeSansBold24pt7b);
|
||||
_currentFont = &FreeSansBold24pt7b;
|
||||
_canvas->setTextSize(1);
|
||||
break;
|
||||
case 5: // Clock face — lock screen (FreeSansBold24pt scaled 5×)
|
||||
case 5: // Clock face -- lock screen (FreeSansBold24pt scaled 5x)
|
||||
_canvas->setFont(&FreeSansBold24pt7b);
|
||||
_currentFont = &FreeSansBold24pt7b;
|
||||
_currentTextScale = 5;
|
||||
_canvas->setTextSize(5);
|
||||
break;
|
||||
default:
|
||||
#ifdef MECK_SERIF_FONT
|
||||
_canvas->setFont(&FreeSerif12pt7b);
|
||||
_currentFont = &FreeSerif12pt7b;
|
||||
#else
|
||||
_canvas->setFont(&FreeSans12pt7b);
|
||||
_currentFont = &FreeSans12pt7b;
|
||||
#endif
|
||||
_canvas->setTextSize(1);
|
||||
break;
|
||||
@@ -203,7 +298,79 @@ void FastEPDDisplay::setCursor(int x, int y) {
|
||||
void FastEPDDisplay::print(const char* str) {
|
||||
if (!_canvas || !str) return;
|
||||
_frameCRC.update<char>(str, strlen(str));
|
||||
_canvas->print(str);
|
||||
|
||||
if (!hasNonAscii(str)) {
|
||||
// Pure ASCII fast path
|
||||
_canvas->print(str);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* s = (const uint8_t*)str;
|
||||
int len = strlen(str);
|
||||
int pos = 0;
|
||||
bool has8bFont = _currentFont && _currentFont->last > 0xFF;
|
||||
|
||||
while (pos < len) {
|
||||
uint8_t b = s[pos];
|
||||
if (b < 0x80) {
|
||||
_canvas->write(b);
|
||||
pos++;
|
||||
} else {
|
||||
int consumed;
|
||||
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
|
||||
if (has8bFont && cp >= _currentFont->first && cp <= _currentFont->last) {
|
||||
drawGlyphAtCursor((uint16_t)cp);
|
||||
} else if (!has8bFont) {
|
||||
char folded = foldToAscii(cp);
|
||||
if (folded) _canvas->write((uint8_t)folded);
|
||||
}
|
||||
pos += consumed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FastEPDDisplay::drawGlyphAtCursor(uint16_t cp) {
|
||||
if (!_canvas || !_currentFont || cp < _currentFont->first || cp > _currentFont->last) return;
|
||||
|
||||
uint16_t idx = cp - _currentFont->first;
|
||||
GFXglyph* glyph = &_currentFont->glyph[idx];
|
||||
|
||||
uint8_t gw = glyph->width;
|
||||
uint8_t gh = glyph->height;
|
||||
int8_t xo = glyph->xOffset;
|
||||
int8_t yo = glyph->yOffset;
|
||||
uint16_t bo = glyph->bitmapOffset;
|
||||
uint8_t xa = glyph->xAdvance;
|
||||
|
||||
int16_t cx = _canvas->getCursorX();
|
||||
int16_t cy = _canvas->getCursorY();
|
||||
|
||||
// Canvas color: 0 = black, 1 = white
|
||||
uint16_t canvasColor = (_curr_color == GxEPD_BLACK) ? 0 : 1;
|
||||
|
||||
if (gw > 0 && gh > 0) {
|
||||
const uint8_t* bitmap = _currentFont->bitmap;
|
||||
uint8_t bit = 0, bits = 0;
|
||||
|
||||
for (uint8_t yy = 0; yy < gh; yy++) {
|
||||
for (uint8_t xx = 0; xx < gw; xx++) {
|
||||
if (!(bit++ & 7)) bits = bitmap[bo++];
|
||||
if (bits & 0x80) {
|
||||
if (_currentTextScale == 1) {
|
||||
_canvas->drawPixel(cx + xo + xx, cy + yo + yy, canvasColor);
|
||||
} else {
|
||||
_canvas->fillRect(
|
||||
cx + (xo + xx) * _currentTextScale,
|
||||
cy + (yo + yy) * _currentTextScale,
|
||||
_currentTextScale, _currentTextScale, canvasColor);
|
||||
}
|
||||
}
|
||||
bits <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_canvas->setCursor(cx + xa * _currentTextScale, cy);
|
||||
}
|
||||
|
||||
void FastEPDDisplay::fillRect(int x, int y, int w, int h) {
|
||||
@@ -277,9 +444,54 @@ void FastEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
|
||||
|
||||
uint16_t FastEPDDisplay::getTextWidth(const char* str) {
|
||||
if (!_canvas || !str) return 0;
|
||||
|
||||
if (!hasNonAscii(str)) {
|
||||
int16_t x1, y1;
|
||||
uint16_t w, h;
|
||||
_canvas->getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
||||
return (uint16_t)ceil((w + 1) / scale_x);
|
||||
}
|
||||
|
||||
bool has8bFont = _currentFont && _currentFont->last > 0xFF;
|
||||
|
||||
if (has8bFont) {
|
||||
const uint8_t* s = (const uint8_t*)str;
|
||||
int len = strlen(str);
|
||||
int pos = 0;
|
||||
int totalAdv = 0;
|
||||
while (pos < len) {
|
||||
int consumed;
|
||||
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
|
||||
if (cp >= _currentFont->first && cp <= _currentFont->last) {
|
||||
totalAdv += _currentFont->glyph[cp - _currentFont->first].xAdvance * _currentTextScale;
|
||||
}
|
||||
pos += consumed;
|
||||
}
|
||||
return (uint16_t)ceil((totalAdv + 1) / scale_x);
|
||||
}
|
||||
|
||||
// Classic/7b: fold to ASCII, then measure
|
||||
char folded[256];
|
||||
const uint8_t* s = (const uint8_t*)str;
|
||||
int len = strlen(str);
|
||||
int pos = 0, fi = 0;
|
||||
while (pos < len && fi < 254) {
|
||||
uint8_t b = s[pos];
|
||||
if (b < 0x80) {
|
||||
folded[fi++] = (char)b;
|
||||
pos++;
|
||||
} else {
|
||||
int consumed;
|
||||
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
|
||||
char fc = foldToAscii(cp);
|
||||
if (fc) folded[fi++] = fc;
|
||||
pos += consumed;
|
||||
}
|
||||
}
|
||||
folded[fi] = 0;
|
||||
int16_t x1, y1;
|
||||
uint16_t w, h;
|
||||
_canvas->getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
||||
_canvas->getTextBounds(folded, 0, 0, &x1, &y1, &w, &h);
|
||||
return (uint16_t)ceil((w + 1) / scale_x);
|
||||
}
|
||||
|
||||
@@ -328,6 +540,35 @@ void FastEPDDisplay::endFrame() {
|
||||
_epd->backupPlane();
|
||||
}
|
||||
|
||||
void FastEPDDisplay::translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) {
|
||||
if (_currentFont && _currentFont->last > 0xFF) {
|
||||
strncpy(dest, src, dest_size - 1);
|
||||
dest[dest_size - 1] = '\0';
|
||||
return;
|
||||
}
|
||||
// Classic/7b: fold accented chars to ASCII
|
||||
size_t j = 0;
|
||||
const uint8_t* s = (const uint8_t*)src;
|
||||
int len = strlen(src);
|
||||
int pos = 0;
|
||||
while (pos < len && j < dest_size - 1) {
|
||||
uint8_t b = s[pos];
|
||||
if (b >= 32 && b <= 126) {
|
||||
dest[j++] = (char)b;
|
||||
pos++;
|
||||
} else if (b >= 0x80) {
|
||||
int consumed;
|
||||
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
|
||||
char folded = foldToAscii(cp);
|
||||
if (folded) dest[j++] = folded;
|
||||
pos += consumed;
|
||||
} else {
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
dest[j] = 0;
|
||||
}
|
||||
|
||||
void FastEPDDisplay::setDarkMode(bool dark) {
|
||||
_darkMode = dark;
|
||||
_lastCRC = 0; // Force redraw
|
||||
|
||||
@@ -78,7 +78,12 @@ class FastEPDDisplay : public DisplayDriver {
|
||||
uint32_t _lastUpdateMs = 0; // Rate limiting — minimum interval between refreshes
|
||||
bool _forcePartial = false; // When true, use partial updates (VKB typing)
|
||||
bool _darkMode = false; // Invert all pixels (black bg, white text)
|
||||
bool _portraitMode = false; // Rotated 90° (540×960 logical)
|
||||
bool _portraitMode = false; // Rotated 90 (540x960 logical)
|
||||
const GFXfont* _currentFont = nullptr; // Track for UTF-8 rendering
|
||||
uint8_t _currentTextScale = 1; // Track glyph scale factor
|
||||
|
||||
// Render one glyph from the current 8b font at the canvas cursor position
|
||||
void drawGlyphAtCursor(uint16_t cp);
|
||||
|
||||
// Virtual 128×128 → physical canvas mapping (runtime, changes with portrait)
|
||||
float scale_x = 7.5f; // 960 / 128 (landscape default)
|
||||
@@ -106,6 +111,7 @@ public:
|
||||
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
|
||||
uint16_t getTextWidth(const char* str) override;
|
||||
void endFrame() override;
|
||||
void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override;
|
||||
|
||||
// --- Raw pixel access for MapScreen (bypasses scaling) ---
|
||||
void drawPixelRaw(int16_t x, int16_t y, uint16_t color) {
|
||||
|
||||
@@ -9,6 +9,101 @@
|
||||
#define DISPLAY_ROTATION 0
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UTF-8 helpers for diacritic / extended Latin rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Decode one UTF-8 character. Returns codepoint, sets *consumed to byte count.
|
||||
static uint32_t utf8Decode(const uint8_t* s, int len, int* consumed) {
|
||||
if (len <= 0) { *consumed = 0; return 0; }
|
||||
uint8_t b = s[0];
|
||||
if (b < 0x80) { *consumed = 1; return b; }
|
||||
if ((b & 0xE0) == 0xC0 && len >= 2 && (s[1] & 0xC0) == 0x80) {
|
||||
*consumed = 2;
|
||||
return ((uint32_t)(b & 0x1F) << 6) | (s[1] & 0x3F);
|
||||
}
|
||||
if ((b & 0xF0) == 0xE0 && len >= 3 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80) {
|
||||
*consumed = 3;
|
||||
return ((uint32_t)(b & 0x0F) << 12) | ((uint32_t)(s[1] & 0x3F) << 6) | (s[2] & 0x3F);
|
||||
}
|
||||
if ((b & 0xF8) == 0xF0 && len >= 4 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80 && (s[3] & 0xC0) == 0x80) {
|
||||
*consumed = 4;
|
||||
return ((uint32_t)(b & 0x07) << 18) | ((uint32_t)(s[1] & 0x3F) << 12) | ((uint32_t)(s[2] & 0x3F) << 6) | (s[3] & 0x3F);
|
||||
}
|
||||
// Invalid lead byte or truncated sequence -- skip one byte
|
||||
*consumed = 1;
|
||||
return 0xFFFD; // replacement character
|
||||
}
|
||||
|
||||
// Fold a Unicode codepoint to its ASCII base letter.
|
||||
// Returns the ASCII char, or 0 if no folding exists.
|
||||
static char foldToAscii(uint32_t cp) {
|
||||
if (cp < 0x80) return (char)cp;
|
||||
|
||||
// Latin-1 Supplement (U+00C0 -- U+00FF)
|
||||
if (cp >= 0xC0 && cp <= 0xFF) {
|
||||
// C0 CF
|
||||
static const char f[64] = {
|
||||
'A','A','A','A','A','A','A','C','E','E','E','E','I','I','I','I',
|
||||
'D','N','O','O','O','O','O', 0 ,'O','U','U','U','U','Y', 0 ,'s',
|
||||
'a','a','a','a','a','a','a','c','e','e','e','e','i','i','i','i',
|
||||
'd','n','o','o','o','o','o', 0 ,'o','u','u','u','u','y', 0 ,'y'
|
||||
};
|
||||
return f[cp - 0xC0];
|
||||
}
|
||||
|
||||
// Latin Extended-A (U+0100 -- U+017F) -- Czech carons, Polish, etc.
|
||||
if (cp >= 0x100 && cp <= 0x17F) {
|
||||
switch (cp) {
|
||||
case 0x100: case 0x102: case 0x104: return 'A';
|
||||
case 0x101: case 0x103: case 0x105: return 'a';
|
||||
case 0x106: case 0x108: case 0x10A: case 0x10C: return 'C';
|
||||
case 0x107: case 0x109: case 0x10B: case 0x10D: return 'c';
|
||||
case 0x10E: case 0x110: return 'D';
|
||||
case 0x10F: case 0x111: return 'd';
|
||||
case 0x112: case 0x114: case 0x116: case 0x118: case 0x11A: return 'E';
|
||||
case 0x113: case 0x115: case 0x117: case 0x119: case 0x11B: return 'e';
|
||||
case 0x11C: case 0x11E: case 0x120: case 0x122: return 'G';
|
||||
case 0x11D: case 0x11F: case 0x121: case 0x123: return 'g';
|
||||
case 0x124: case 0x126: return 'H';
|
||||
case 0x125: case 0x127: return 'h';
|
||||
case 0x128: case 0x12A: case 0x12C: case 0x12E: case 0x130: return 'I';
|
||||
case 0x129: case 0x12B: case 0x12D: case 0x12F: case 0x131: return 'i';
|
||||
case 0x134: return 'J'; case 0x135: return 'j';
|
||||
case 0x136: return 'K'; case 0x137: case 0x138: return 'k';
|
||||
case 0x139: case 0x13B: case 0x13D: case 0x13F: case 0x141: return 'L';
|
||||
case 0x13A: case 0x13C: case 0x13E: case 0x140: case 0x142: return 'l';
|
||||
case 0x143: case 0x145: case 0x147: return 'N';
|
||||
case 0x144: case 0x146: case 0x148: case 0x149: return 'n';
|
||||
case 0x14C: case 0x14E: case 0x150: return 'O';
|
||||
case 0x14D: case 0x14F: case 0x151: return 'o';
|
||||
case 0x152: return 'O'; case 0x153: return 'o'; // OE ligature
|
||||
case 0x154: case 0x156: case 0x158: return 'R';
|
||||
case 0x155: case 0x157: case 0x159: return 'r';
|
||||
case 0x15A: case 0x15C: case 0x15E: case 0x160: return 'S';
|
||||
case 0x15B: case 0x15D: case 0x15F: case 0x161: return 's';
|
||||
case 0x162: case 0x164: case 0x166: return 'T';
|
||||
case 0x163: case 0x165: case 0x167: return 't';
|
||||
case 0x168: case 0x16A: case 0x16C: case 0x16E: case 0x170: case 0x172: return 'U';
|
||||
case 0x169: case 0x16B: case 0x16D: case 0x16F: case 0x171: case 0x173: return 'u';
|
||||
case 0x174: return 'W'; case 0x175: return 'w';
|
||||
case 0x176: case 0x178: return 'Y'; case 0x177: return 'y';
|
||||
case 0x179: case 0x17B: case 0x17D: return 'Z';
|
||||
case 0x17A: case 0x17C: case 0x17E: return 'z';
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // not foldable
|
||||
}
|
||||
|
||||
// Check if a string contains any non-ASCII bytes
|
||||
static bool hasNonAscii(const char* str) {
|
||||
for (int i = 0; str[i]; i++) {
|
||||
if ((uint8_t)str[i] >= 0x80) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef ESP32
|
||||
// E-ink and LoRa SHARE the same SPI bus (SCK=36, MOSI=33)
|
||||
// They MUST use the same SPI peripheral (HSPI) to avoid GPIO conflicts
|
||||
@@ -114,36 +209,46 @@ void GxEPDDisplay::setTextSize(int sz) {
|
||||
const GFXfont* customFont = meckGetFont(_fontStyle, sz);
|
||||
if (customFont) {
|
||||
display.setFont(customFont);
|
||||
// textSize 5 (clock face) uses ×2 scaling even with custom fonts
|
||||
display.setTextSize(sz == 5 ? 2 : 1);
|
||||
_currentFont = customFont;
|
||||
// textSize 5 (clock face) uses x2 scaling even with custom fonts
|
||||
_currentTextScale = (sz == 5) ? 2 : 1;
|
||||
display.setTextSize(_currentTextScale);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Classic style (or fallback) — original FreeSans fonts
|
||||
// Classic style (or fallback) -- original FreeSans fonts
|
||||
_currentTextScale = 1;
|
||||
switch(sz) {
|
||||
case 0: // Tiny - built-in 6x8 pixel font
|
||||
display.setFont(NULL);
|
||||
_currentFont = nullptr;
|
||||
display.setTextSize(1);
|
||||
break;
|
||||
case 1: // Small - use 9pt (was 9pt)
|
||||
display.setFont(&FreeSans9pt7b);
|
||||
_currentFont = &FreeSans9pt7b;
|
||||
display.setTextSize(1);
|
||||
break;
|
||||
case 2: // Medium Bold - use 9pt bold instead of 12pt
|
||||
display.setFont(&FreeSans9pt7b);
|
||||
_currentFont = &FreeSans9pt7b;
|
||||
display.setTextSize(1);
|
||||
break;
|
||||
case 3: // Large - use 12pt instead of 18pt
|
||||
display.setFont(&FreeSansBold12pt7b);
|
||||
_currentFont = &FreeSansBold12pt7b;
|
||||
display.setTextSize(1);
|
||||
break;
|
||||
case 5: // Extra Large - lock screen clock face
|
||||
display.setFont(&FreeSansBold12pt7b);
|
||||
display.setTextSize(2); // GxEPD2 native 2× scaling on 12pt bold
|
||||
_currentFont = &FreeSansBold12pt7b;
|
||||
_currentTextScale = 2;
|
||||
display.setTextSize(2); // GxEPD2 native 2x scaling on 12pt bold
|
||||
break;
|
||||
default:
|
||||
display.setFont(&FreeSans9pt7b);
|
||||
_currentFont = &FreeSans9pt7b;
|
||||
display.setTextSize(1);
|
||||
break;
|
||||
}
|
||||
@@ -177,7 +282,89 @@ void GxEPDDisplay::setCursor(int x, int y) {
|
||||
|
||||
void GxEPDDisplay::print(const char* str) {
|
||||
display_crc.update<char>(str, strlen(str));
|
||||
display.print(str);
|
||||
|
||||
if (!hasNonAscii(str)) {
|
||||
// Pure ASCII fast path -- no decoding needed
|
||||
display.print(str);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* s = (const uint8_t*)str;
|
||||
int len = strlen(str);
|
||||
int pos = 0;
|
||||
|
||||
// Check if current font is an 8b font (covers codepoints > 0xFF)
|
||||
bool has8bFont = _currentFont && _currentFont->last > 0xFF;
|
||||
|
||||
while (pos < len) {
|
||||
uint8_t b = s[pos];
|
||||
|
||||
if (b < 0x80) {
|
||||
// ASCII: use normal Adafruit GFX path (handles newlines etc.)
|
||||
display.write(b);
|
||||
pos++;
|
||||
} else {
|
||||
// Multi-byte UTF-8 sequence
|
||||
int consumed;
|
||||
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
|
||||
|
||||
if (has8bFont && cp >= _currentFont->first && cp <= _currentFont->last) {
|
||||
// Render directly from 8b font glyph table
|
||||
drawGlyphAtCursor((uint16_t)cp);
|
||||
} else if (!has8bFont) {
|
||||
// Classic/7b font -- fold to ASCII
|
||||
char folded = foldToAscii(cp);
|
||||
if (folded) display.write((uint8_t)folded);
|
||||
}
|
||||
// else: codepoint outside font range, skip
|
||||
pos += consumed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render one glyph from the current 8b font at the display cursor position.
|
||||
// Mimics Adafruit_GFX::drawChar() but supports 16-bit codepoints.
|
||||
void GxEPDDisplay::drawGlyphAtCursor(uint16_t cp) {
|
||||
if (!_currentFont || cp < _currentFont->first || cp > _currentFont->last) return;
|
||||
|
||||
uint16_t idx = cp - _currentFont->first;
|
||||
GFXglyph* glyph = &_currentFont->glyph[idx];
|
||||
|
||||
uint8_t gw = glyph->width;
|
||||
uint8_t gh = glyph->height;
|
||||
int8_t xo = glyph->xOffset;
|
||||
int8_t yo = glyph->yOffset;
|
||||
uint16_t bo = glyph->bitmapOffset;
|
||||
uint8_t xa = glyph->xAdvance;
|
||||
|
||||
int16_t cx = display.getCursorX();
|
||||
int16_t cy = display.getCursorY();
|
||||
|
||||
if (gw > 0 && gh > 0) {
|
||||
const uint8_t* bitmap = _currentFont->bitmap;
|
||||
uint8_t bit = 0, bits = 0;
|
||||
|
||||
for (uint8_t yy = 0; yy < gh; yy++) {
|
||||
for (uint8_t xx = 0; xx < gw; xx++) {
|
||||
if (!(bit++ & 7)) bits = bitmap[bo++];
|
||||
if (bits & 0x80) {
|
||||
if (_currentTextScale == 1) {
|
||||
display.drawPixel(cx + xo + xx, cy + yo + yy, _curr_color);
|
||||
} else {
|
||||
// Scaled rendering (clock face etc.)
|
||||
display.fillRect(
|
||||
cx + (xo + xx) * _currentTextScale,
|
||||
cy + (yo + yy) * _currentTextScale,
|
||||
_currentTextScale, _currentTextScale, _curr_color);
|
||||
}
|
||||
}
|
||||
bits <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Advance cursor by xAdvance (scaled)
|
||||
display.setCursor(cx + xa * _currentTextScale, cy);
|
||||
}
|
||||
|
||||
void GxEPDDisplay::fillRect(int x, int y, int w, int h) {
|
||||
@@ -238,9 +425,55 @@ void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
|
||||
}
|
||||
|
||||
uint16_t GxEPDDisplay::getTextWidth(const char* str) {
|
||||
if (!hasNonAscii(str)) {
|
||||
// Pure ASCII fast path
|
||||
int16_t x1, y1;
|
||||
uint16_t w, h;
|
||||
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
||||
return ceil((w + 1) / scale_x);
|
||||
}
|
||||
|
||||
bool has8bFont = _currentFont && _currentFont->last > 0xFF;
|
||||
|
||||
if (has8bFont) {
|
||||
// 8b font: sum xAdvance for each decoded codepoint
|
||||
const uint8_t* s = (const uint8_t*)str;
|
||||
int len = strlen(str);
|
||||
int pos = 0;
|
||||
int totalAdv = 0;
|
||||
while (pos < len) {
|
||||
int consumed;
|
||||
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
|
||||
if (cp >= _currentFont->first && cp <= _currentFont->last) {
|
||||
totalAdv += _currentFont->glyph[cp - _currentFont->first].xAdvance * _currentTextScale;
|
||||
}
|
||||
pos += consumed;
|
||||
}
|
||||
return ceil((totalAdv + 1) / scale_x);
|
||||
}
|
||||
|
||||
// Classic/7b: fold to ASCII, then measure the folded string
|
||||
char folded[256];
|
||||
const uint8_t* s = (const uint8_t*)str;
|
||||
int len = strlen(str);
|
||||
int pos = 0, fi = 0;
|
||||
while (pos < len && fi < 254) {
|
||||
uint8_t b = s[pos];
|
||||
if (b < 0x80) {
|
||||
folded[fi++] = (char)b;
|
||||
pos++;
|
||||
} else {
|
||||
int consumed;
|
||||
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
|
||||
char fc = foldToAscii(cp);
|
||||
if (fc) folded[fi++] = fc;
|
||||
pos += consumed;
|
||||
}
|
||||
}
|
||||
folded[fi] = 0;
|
||||
int16_t x1, y1;
|
||||
uint16_t w, h;
|
||||
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
||||
display.getTextBounds(folded, 0, 0, &x1, &y1, &w, &h);
|
||||
return ceil((w + 1) / scale_x);
|
||||
}
|
||||
|
||||
@@ -254,4 +487,35 @@ void GxEPDDisplay::endFrame() {
|
||||
#endif
|
||||
last_display_crc_value = crc;
|
||||
}
|
||||
}
|
||||
|
||||
void GxEPDDisplay::translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) {
|
||||
// If an 8b font is active, pass through UTF-8 bytes (font can render them)
|
||||
if (_currentFont && _currentFont->last > 0xFF) {
|
||||
strncpy(dest, src, dest_size - 1);
|
||||
dest[dest_size - 1] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Classic/7b font: fold accented chars to ASCII base letters
|
||||
size_t j = 0;
|
||||
const uint8_t* s = (const uint8_t*)src;
|
||||
int len = strlen(src);
|
||||
int pos = 0;
|
||||
while (pos < len && j < dest_size - 1) {
|
||||
uint8_t b = s[pos];
|
||||
if (b >= 32 && b <= 126) {
|
||||
dest[j++] = (char)b;
|
||||
pos++;
|
||||
} else if (b >= 0x80) {
|
||||
int consumed;
|
||||
uint32_t cp = utf8Decode(s + pos, len - pos, &consumed);
|
||||
char folded = foldToAscii(cp);
|
||||
if (folded) dest[j++] = folded;
|
||||
pos += consumed;
|
||||
} else {
|
||||
pos++; // skip control chars
|
||||
}
|
||||
}
|
||||
dest[j] = 0;
|
||||
}
|
||||
@@ -19,6 +19,13 @@
|
||||
#include <Fonts/FreeSansBold12pt7b.h>
|
||||
#include <Fonts/FreeSans18pt7b.h>
|
||||
|
||||
// Meck custom font styles (Noto Sans, Montserrat)
|
||||
// Define HAS_MECK_FONTS in platformio.ini build_flags for platforms with
|
||||
// enough flash to carry the extra font data (~50KB for 8b Latin Extended).
|
||||
#ifdef HAS_MECK_FONTS
|
||||
#include "MeckFonts.h"
|
||||
#endif
|
||||
|
||||
// Inline CRC32 for frame change detection (replaces bakercp/CRC32
|
||||
// to avoid naming collision with PNGdec's bundled CRC32.h)
|
||||
class FrameCRC32 {
|
||||
@@ -67,6 +74,11 @@ class GxEPDDisplay : public DisplayDriver {
|
||||
uint16_t _curr_color;
|
||||
FrameCRC32 display_crc;
|
||||
int last_display_crc_value = 0;
|
||||
const GFXfont* _currentFont = nullptr; // Track for UTF-8 rendering
|
||||
uint8_t _currentTextScale = 1; // Track glyph scale factor
|
||||
|
||||
// Render one glyph from the current 8b font at the display's cursor position
|
||||
void drawGlyphAtCursor(uint16_t cp);
|
||||
|
||||
public:
|
||||
// Virtual canvas dimensions — default 128×128 (MeshCore standard).
|
||||
@@ -104,6 +116,7 @@ public:
|
||||
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
|
||||
uint16_t getTextWidth(const char* str) override;
|
||||
void endFrame() override;
|
||||
void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override;
|
||||
|
||||
// --- Raw pixel access for MapScreen (bypasses scaling) ---
|
||||
void drawPixelRaw(int16_t x, int16_t y, uint16_t color) {
|
||||
|
||||
Reference in New Issue
Block a user