mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
feat(eink): landscape and portrait builds, 8x13 font in landscape
The L1 e-ink solo build splits in two, one per screen shape, so each screen can be laid out for a single shape. Settings > Rotation now only turns the screen upside down, and the joystick turns with it. Landscape writes in misc-fixed 8x13 at the panel's own resolution (EINK_LARGE_FONT); portrait keeps the 6x9. All drawing moves into EinkGfxDisplay, which the panel driver and the simulator share; the FreeSans fonts and the CRC32 library go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,6 +46,13 @@
|
||||
// would split the build the day this is flipped to 1.
|
||||
#define FEAT_RX_POWERSAVE 0
|
||||
|
||||
// The display rotation at which the joystick's own up reads as up. An e-ink
|
||||
// panel turned from it turns the joystick with it (UITask), on top of the
|
||||
// Joystick setting. Unset, the build's own rotation: only a flip turns it.
|
||||
#if defined(DISPLAY_ROTATION) && !defined(JOYSTICK_UPRIGHT_ROTATION)
|
||||
#define JOYSTICK_UPRIGHT_ROTATION DISPLAY_ROTATION
|
||||
#endif
|
||||
|
||||
namespace Features {
|
||||
|
||||
#if defined(EINK_DISPLAY_MODEL)
|
||||
|
||||
@@ -538,7 +538,7 @@ class SettingsScreen : public UIScreen {
|
||||
#endif
|
||||
#if FEAT_DISPLAY_ROTATION_SETTING
|
||||
} else if (item == ROTATION) {
|
||||
val("Rotation", rotLabel(p ? p->display_rotation : 0));
|
||||
val("Rotation", rotLabel(p ? p->display_rotation - DISPLAY_ROTATION : 0));
|
||||
#endif
|
||||
#if FEAT_JOYSTICK_ROTATION_SETTING
|
||||
} else if (item == JOY_ROTATION) {
|
||||
@@ -1040,7 +1040,7 @@ public:
|
||||
#endif
|
||||
#if FEAT_DISPLAY_ROTATION_SETTING
|
||||
if (_selected == ROTATION && p && (left || right || enter)) {
|
||||
p->display_rotation = (p->display_rotation + (left ? 3 : 1)) & 3;
|
||||
p->display_rotation ^= 2; // the build's shape, either way up
|
||||
_task->applyRotation();
|
||||
_dirty = true;
|
||||
return true;
|
||||
|
||||
@@ -2803,6 +2803,9 @@ void UITask::loop() {
|
||||
if (status_screen) ((StatusScreen*)status_screen)->sample(); // Status history lines, once a minute
|
||||
#if UI_HAS_JOYSTICK
|
||||
uint8_t joy_rot = _node_prefs ? _node_prefs->joystick_rotation : JOYSTICK_ROTATION;
|
||||
#if FEAT_DISPLAY_ROTATION_SETTING && defined(JOYSTICK_UPRIGHT_ROTATION)
|
||||
if (_node_prefs) joy_rot += (JOYSTICK_UPRIGHT_ROTATION - _node_prefs->display_rotation) & 3;
|
||||
#endif
|
||||
int ev = user_btn.check();
|
||||
if (ev == BUTTON_EVENT_CLICK) {
|
||||
if (back_btn.isPressed() && !_unlock_kb) {
|
||||
@@ -3606,6 +3609,13 @@ void UITask::applyBrightness() {
|
||||
|
||||
void UITask::applyRotation() {
|
||||
if (_display != NULL && _node_prefs != NULL) {
|
||||
#if FEAT_DISPLAY_ROTATION_SETTING && defined(DISPLAY_ROTATION)
|
||||
// Each build is laid out for one shape and only turns it upside down: a
|
||||
// rotation of the other shape (a fresh device's 0, another build's, or
|
||||
// from before the split) comes back to this build's own.
|
||||
uint8_t& rot = _node_prefs->display_rotation;
|
||||
if ((rot ^ DISPLAY_ROTATION) & 1) rot = DISPLAY_ROTATION & 3;
|
||||
#endif
|
||||
_display->setDisplayRotation(_node_prefs->display_rotation);
|
||||
_next_refresh = 0;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,14 @@
|
||||
; variants in variants/, so this repo's Solo-specific additions don't get
|
||||
; mixed in with configs other MeshCore forks/upstream also carry.
|
||||
;
|
||||
; Dual BLE+USB with added Solo
|
||||
[env:WioTrackerL1Eink_companion_solo_dual]
|
||||
; Two builds, one per orientation: each screen is laid out for one shape, and
|
||||
; Settings > Rotation only turns it upside down. Landscape writes in the 8x13
|
||||
; font at the panel's own resolution (EINK_LARGE_FONT); portrait keeps the 6x9,
|
||||
; whose narrow width wants the smaller cell. The joystick reads upright with the
|
||||
; panel at rotation 1 (landscape) and turns with it (JOYSTICK_UPRIGHT_ROTATION).
|
||||
;
|
||||
; Both: Dual BLE+USB with added Solo.
|
||||
[eink_solo]
|
||||
extends = WioTrackerL1Eink
|
||||
; Optimise for size, not speed: the nRF52 Arduino core defaults to -Ofast
|
||||
; (≈ -O3 + -ffast-math), which bloats flash by ~200 KB on this feature-rich
|
||||
@@ -14,10 +20,22 @@ extends = WioTrackerL1Eink
|
||||
; The offline queue (messages held for the phone app while it's away) at 128,
|
||||
; not the board's 256: on the nRF52 the 256 x 177 B left ~5 KB of heap next to
|
||||
; the on-device UI (which keeps its own history), too little for a sign buffer.
|
||||
build_unflags = -Ofast -D OFFLINE_QUEUE_SIZE=256
|
||||
build_unflags = -Ofast -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_ROTATION=1
|
||||
build_flags = ${WioTrackerL1Eink.build_flags}
|
||||
-D OFFLINE_QUEUE_SIZE=128
|
||||
-D DUAL_SERIAL=1
|
||||
-D ENABLE_SCREENSHOT
|
||||
-D JOYSTICK_UPRIGHT_ROTATION=1
|
||||
-Os
|
||||
extra_scripts = post:create-uf2.py
|
||||
|
||||
[env:WioTrackerL1Eink_landscape_solo_dual]
|
||||
extends = eink_solo
|
||||
build_flags = ${eink_solo.build_flags}
|
||||
-D DISPLAY_ROTATION=1
|
||||
-D EINK_LARGE_FONT=1
|
||||
|
||||
[env:WioTrackerL1Eink_portrait_solo_dual]
|
||||
extends = eink_solo
|
||||
build_flags = ${eink_solo.build_flags}
|
||||
-D DISPLAY_ROTATION=0
|
||||
|
||||
@@ -81,8 +81,11 @@ public:
|
||||
#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; }
|
||||
// How many panel pixels draw one layout pixel: 2 where a driver doubles
|
||||
// everything (landscape e-ink without its large font), else 1.
|
||||
virtual int pixelScale() const { return 1; }
|
||||
// separator line thickness: one layout pixel
|
||||
int sepH() const { return pixelScale(); }
|
||||
virtual void drawTextCentered(int mid_x, int y, const char* str) {
|
||||
char tmp[256]; translateUTF8ToBlocks(tmp, str, sizeof(tmp));
|
||||
int w = getTextWidth(tmp);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include <gfxfont.h>
|
||||
#include "DisplayDriver.h"
|
||||
|
||||
class Adafruit_GFX;
|
||||
|
||||
// Everything an e-ink panel draws, on top of any Adafruit_GFX: text in the
|
||||
// misc-fixed font, rects, bitmaps, and a hash of each frame so an unchanged
|
||||
// one never costs a refresh. GxEPDDisplay puts it on a real panel; the
|
||||
// simulator's SimEinkDisplay on a canvas, so the two render identically.
|
||||
//
|
||||
// The bodies live in EinkGfxDisplayImpl.h, which exactly one .cpp includes
|
||||
// (GxEPDDisplay.cpp on a board, the sim's target.cpp): it pulls in the font
|
||||
// tables.
|
||||
|
||||
// EINK_LARGE_FONT: a landscape panel writes in the 8x13 at its own resolution.
|
||||
// Without it a landscape panel doubles the 6x9 (every pixel 2x2) -- for small
|
||||
// dense panels where 8x13 would be too fine. A portrait panel always uses the
|
||||
// 6x9: its narrow width wants the smaller cell.
|
||||
#ifndef EINK_LARGE_FONT
|
||||
#define EINK_LARGE_FONT 0
|
||||
#endif
|
||||
|
||||
class EinkGfxDisplay : public DisplayDriver {
|
||||
Adafruit_GFX& _gfx;
|
||||
const uint16_t _ink, _paper; // LIGHT and DARK in the panel's colours
|
||||
uint16_t _curr;
|
||||
const GFXfont* _font;
|
||||
int _cell_w = 6, _cell_h = 9; // the font's cell, before _px and the text size
|
||||
int _px = 1; // pixel doubling (landscape without EINK_LARGE_FONT)
|
||||
int _text_sz = 1;
|
||||
int _cx = 0, _cy = 0; // cursor, top of the text row
|
||||
uint32_t _hash = 0, _shown_hash = 0;
|
||||
bool _force = true;
|
||||
|
||||
void mix(const void* p, size_t n) {
|
||||
const uint8_t* b = (const uint8_t*)p;
|
||||
while (n--) { _hash ^= *b++; _hash *= 16777619u; } // FNV-1a
|
||||
}
|
||||
template <class T> void mix(T v) { mix(&v, sizeof(v)); }
|
||||
|
||||
protected:
|
||||
EinkGfxDisplay(int w, int h, Adafruit_GFX& gfx, uint16_t ink, uint16_t paper);
|
||||
|
||||
// After a size or rotation change: picks the font for the new shape.
|
||||
void updateLayout();
|
||||
// At the end of a frame: whether it differs from the one on the panel
|
||||
// (and from now on counts as shown).
|
||||
bool frameChanged() {
|
||||
if (!_force && _hash == _shown_hash) return false;
|
||||
_shown_hash = _hash;
|
||||
_force = false;
|
||||
return true;
|
||||
}
|
||||
void forceRedraw() { _force = true; }
|
||||
|
||||
public:
|
||||
// Size 4 is the huge built-in font (6x8 cell x 7), only for the stacked
|
||||
// HH / MM clock on a portrait panel. Sizes 1-3 are the misc-fixed font at
|
||||
// that multiple, as on the OLEDs.
|
||||
static const int BIG_TEXT_SCALE = 7;
|
||||
int getCharWidth() const override {
|
||||
return _text_sz == 4 ? 6 * BIG_TEXT_SCALE : _cell_w * _px * _text_sz;
|
||||
}
|
||||
int getLineHeight() const override {
|
||||
return _text_sz == 4 ? 8 * BIG_TEXT_SCALE : _cell_h * _px * _text_sz;
|
||||
}
|
||||
int pixelScale() const override { return _px; }
|
||||
bool isSingleFont() const override { return true; }
|
||||
void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override {
|
||||
strncpy(dest, src, dest_size - 1);
|
||||
dest[dest_size - 1] = '\0';
|
||||
}
|
||||
bool isEink() override { return true; }
|
||||
|
||||
void startFrame(Color bkg = DARK) override;
|
||||
void setTextSize(int sz) override;
|
||||
void setColor(Color c) override;
|
||||
void setCursor(int x, int y) override;
|
||||
void print(const char* str) override;
|
||||
void fillRect(int x, int y, int w, int h) override;
|
||||
void drawRect(int x, int y, int w, int h) override;
|
||||
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
|
||||
uint16_t getTextWidth(const char* str) override;
|
||||
uint16_t getCodepointWidth(uint32_t cp) override;
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
// EinkGfxDisplay's bodies. Include from exactly one .cpp (see EinkGfxDisplay.h):
|
||||
// the font tables below are static arrays.
|
||||
|
||||
#include "EinkGfxDisplay.h"
|
||||
#include "MiscFixedRenderer.h"
|
||||
#include "MiscFixed8x13Font.h"
|
||||
|
||||
EinkGfxDisplay::EinkGfxDisplay(int w, int h, Adafruit_GFX& gfx, uint16_t ink, uint16_t paper)
|
||||
: DisplayDriver(w, h), _gfx(gfx), _ink(ink), _paper(paper), _curr(ink), _font(&MiscFixed) {
|
||||
updateLayout();
|
||||
}
|
||||
|
||||
void EinkGfxDisplay::updateLayout() {
|
||||
const bool landscape = width() >= height();
|
||||
_font = (EINK_LARGE_FONT && landscape) ? &MiscFixed8x13 : &MiscFixed;
|
||||
_px = (!EINK_LARGE_FONT && landscape) ? 2 : 1;
|
||||
_cell_w = miscFixedCellW(*_font);
|
||||
_cell_h = _font->yAdvance;
|
||||
_vw_dirty = true;
|
||||
_force = true;
|
||||
}
|
||||
|
||||
void EinkGfxDisplay::startFrame(Color bkg) {
|
||||
_gfx.fillScreen(_paper);
|
||||
_curr = _ink;
|
||||
_text_sz = 1;
|
||||
_hash = 2166136261u;
|
||||
}
|
||||
|
||||
void EinkGfxDisplay::setTextSize(int sz) {
|
||||
_text_sz = sz;
|
||||
_vw_dirty = true;
|
||||
mix<int>(sz);
|
||||
}
|
||||
|
||||
void EinkGfxDisplay::setColor(Color c) {
|
||||
mix<Color>(c);
|
||||
_curr = (c == DARK) ? _paper : _ink; // DARK is the paper, LIGHT the ink
|
||||
}
|
||||
|
||||
void EinkGfxDisplay::setCursor(int x, int y) {
|
||||
mix<int>(x);
|
||||
mix<int>(y);
|
||||
_cx = x;
|
||||
_cy = y;
|
||||
}
|
||||
|
||||
void EinkGfxDisplay::print(const char* str) {
|
||||
mix(str, strlen(str));
|
||||
if (_text_sz == 4) { // the built-in font, its cursor at the cell's top left
|
||||
_gfx.setFont(NULL);
|
||||
_gfx.setTextSize(BIG_TEXT_SCALE);
|
||||
_gfx.setTextColor(_curr);
|
||||
_gfx.setCursor(_cx, _cy);
|
||||
for (const char* p = str; *p; p++) _gfx.write((uint8_t)*p);
|
||||
_cx = _gfx.getCursorX();
|
||||
return;
|
||||
}
|
||||
_gfx.setCursor(_cx, _cy);
|
||||
miscFixedPrint(_gfx, str, _px * _text_sz, _curr, *_font);
|
||||
_cx = _gfx.getCursorX();
|
||||
_cy = _gfx.getCursorY();
|
||||
}
|
||||
|
||||
uint16_t EinkGfxDisplay::getTextWidth(const char* str) {
|
||||
if (_text_sz == 4) return strlen(str) * getCharWidth();
|
||||
return miscFixedTextWidth(str, _px * _text_sz, *_font);
|
||||
}
|
||||
|
||||
uint16_t EinkGfxDisplay::getCodepointWidth(uint32_t cp) {
|
||||
if (_text_sz == 4) return getCharWidth();
|
||||
return miscFixedXAdvance(cp, _px * _text_sz, *_font);
|
||||
}
|
||||
|
||||
void EinkGfxDisplay::fillRect(int x, int y, int w, int h) {
|
||||
mix<int>(x); mix<int>(y); mix<int>(w); mix<int>(h);
|
||||
_gfx.fillRect(x, y, w, h, _curr);
|
||||
}
|
||||
|
||||
void EinkGfxDisplay::drawRect(int x, int y, int w, int h) {
|
||||
mix<int>(x); mix<int>(y); mix<int>(w); mix<int>(h);
|
||||
_gfx.drawRect(x, y, w, h, _curr);
|
||||
if (_px == 2 && w > 2 && h > 2) _gfx.drawRect(x + 1, y + 1, w - 2, h - 2, _curr);
|
||||
}
|
||||
|
||||
void EinkGfxDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
|
||||
mix<int>(x); mix<int>(y); mix<int>(w); mix<int>(h);
|
||||
mix<uintptr_t>((uintptr_t)bits);
|
||||
const int widthInBytes = (w + 7) / 8;
|
||||
for (int by = 0; by < h; by++)
|
||||
for (int bx = 0; bx < w; bx++)
|
||||
if (pgm_read_byte(bits + by * widthInBytes + bx / 8) & (0x80 >> (bx & 7)))
|
||||
_gfx.drawPixel(x + bx, y + by, _curr);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "GxEPDDisplay.h"
|
||||
#include "LemonIcons.h"
|
||||
#include "EinkGfxDisplayImpl.h"
|
||||
|
||||
#ifdef EXP_PIN_BACKLIGHT
|
||||
#include <PCA9557.h>
|
||||
@@ -14,68 +14,6 @@
|
||||
SPIClass SPI1 = SPIClass(FSPI);
|
||||
#endif
|
||||
|
||||
// GFX fonts use the baseline as the cursor origin. UI code assumes top-of-cell
|
||||
// coordinates (same convention as the OLED driver). Add the font ascender so
|
||||
// the two conventions match.
|
||||
static int fontAscender(int sz, bool single_font, int scale) {
|
||||
if (sz == 3) return 26; // FreeSans18pt7b: proportional, baseline origin
|
||||
if (sz == 1 && single_font) 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
|
||||
}
|
||||
|
||||
// y is the GFX baseline (display.getCursorY()), which equals original_y + 8*sc.
|
||||
// Pixels are placed at y + yo*sc + row*sc — identical to how GFX would render
|
||||
// a scaled GFX font, but bypassing GFX so multi-byte UTF-8 is decoded correctly.
|
||||
int16_t GxEPDDisplay::drawGlyph(int16_t x, int16_t y, uint32_t cp, int sc) {
|
||||
for (uint8_t i = 0; i < lemonIconCount; i++) {
|
||||
if (pgm_read_dword(&lemonIconCPs[i]) == cp) {
|
||||
const GFXglyph* g = &lemonIconGlyphs[i];
|
||||
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);
|
||||
uint16_t bo = pgm_read_word(&g->bitmapOffset);
|
||||
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(&lemonIconBitmaps[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);
|
||||
}
|
||||
bit >>= 1;
|
||||
}
|
||||
return x + xa * sc;
|
||||
}
|
||||
}
|
||||
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 = &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);
|
||||
uint16_t bo = pgm_read_word(&g->bitmapOffset);
|
||||
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(&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);
|
||||
}
|
||||
bit >>= 1;
|
||||
}
|
||||
return x + xa * sc;
|
||||
}
|
||||
|
||||
uint8_t GxEPDDisplay::glyphXAdvance(uint32_t cp, int sc) {
|
||||
uint8_t xa;
|
||||
if (cp < MiscFixed.first || cp > MiscFixed.last) xa = 6;
|
||||
else xa = pgm_read_byte(&MiscFixedGlyphs[cp - MiscFixed.first].xAdvance);
|
||||
return xa * sc;
|
||||
}
|
||||
|
||||
bool GxEPDDisplay::begin() {
|
||||
display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
||||
#ifdef ESP32
|
||||
@@ -89,7 +27,6 @@ bool GxEPDDisplay::begin() {
|
||||
// and callBusyPump()/setBusyPumpFn() (DisplayDriver.h).
|
||||
display.epd2.setBusyCallback(busyCallbackTrampoline, this);
|
||||
display.setRotation(DISPLAY_ROTATION);
|
||||
setTextSize(1);
|
||||
display.setPartialWindow(0, 0, display.width(), display.height());
|
||||
display.fillScreen(GxEPD_WHITE);
|
||||
display.display(true);
|
||||
@@ -122,162 +59,17 @@ void GxEPDDisplay::turnOff() {
|
||||
|
||||
void GxEPDDisplay::clear() {
|
||||
display.fillScreen(GxEPD_WHITE);
|
||||
display.setTextColor(GxEPD_BLACK);
|
||||
display_crc.reset();
|
||||
}
|
||||
|
||||
void GxEPDDisplay::startFrame(Color bkg) {
|
||||
display.fillScreen(GxEPD_WHITE);
|
||||
display.setTextColor(_curr_color = GxEPD_BLACK);
|
||||
_text_sz = 1;
|
||||
int sc = scale();
|
||||
display.setFont(_single_font ? &MiscFixed : NULL);
|
||||
display.setTextSize(sc);
|
||||
display_crc.reset();
|
||||
}
|
||||
|
||||
void GxEPDDisplay::setTextSize(int sz) {
|
||||
_text_sz = sz;
|
||||
_vw_dirty = true;
|
||||
display_crc.update<int>(sz);
|
||||
// Size 1 scales with orientation: 1× in portrait (≈OLED width), 2× in landscape.
|
||||
// Size 2 always uses 2× built-in (12×16) — fixed because layout Y-positions are hardcoded.
|
||||
// Size 3 always uses FreeSans18pt for large headings.
|
||||
int sc = scale();
|
||||
switch (sz) {
|
||||
case 4:
|
||||
// Huge clock digits: built-in font scaled up. Cursor stays top-left
|
||||
// (fontAscender returns 0 for the built-in font), so layout maths is plain.
|
||||
display.setFont(NULL);
|
||||
display.setTextSize(BIG_TEXT_SCALE);
|
||||
break;
|
||||
case 3:
|
||||
display.setFont(&FreeSans18pt7b);
|
||||
display.setTextSize(1);
|
||||
break;
|
||||
case 2:
|
||||
display.setFont(NULL);
|
||||
display.setTextSize(scale() * 2);
|
||||
break;
|
||||
default:
|
||||
display.setFont(_single_font ? &MiscFixed : NULL);
|
||||
display.setTextSize(sc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void GxEPDDisplay::setColor(Color c) {
|
||||
display_crc.update<Color>(c);
|
||||
// e-ink: DARK background = white paper, LIGHT foreground = black ink
|
||||
if (c == DARK) {
|
||||
display.setTextColor(_curr_color = GxEPD_WHITE);
|
||||
} else {
|
||||
display.setTextColor(_curr_color = GxEPD_BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
void GxEPDDisplay::setCursor(int x, int y) {
|
||||
display_crc.update<int>(x);
|
||||
display_crc.update<int>(y);
|
||||
// Offset y by the font ascender: callers pass top-of-cell y, GFX fonts
|
||||
// expect baseline y. Without this, text would be clipped at the top.
|
||||
int sc = scale();
|
||||
display.setCursor(x, y + fontAscender(_text_sz, _single_font, sc));
|
||||
}
|
||||
|
||||
void GxEPDDisplay::print(const char* str) {
|
||||
display_crc.update<char>(str, strlen(str));
|
||||
// misc-fixed path only for sz=1 — setTextSize(2/3) switches GFX to other fonts.
|
||||
if (_single_font && _text_sz == 1) {
|
||||
int16_t cx = display.getCursorX();
|
||||
int16_t cy = display.getCursorY();
|
||||
const int sc = scale();
|
||||
const uint8_t* p = (const uint8_t*)str;
|
||||
while (*p) {
|
||||
uint32_t cp = decodeCodepoint(p);
|
||||
if (cp == '\n') { cy += MiscFixed.yAdvance * sc; cx = 0; }
|
||||
else { cx = drawGlyph(cx, cy, cp, sc); }
|
||||
}
|
||||
display.setCursor(cx, cy);
|
||||
return;
|
||||
}
|
||||
int sc = scale();
|
||||
for (const char* p = str; *p; p++) {
|
||||
if ((uint8_t)*p == 0xDB) {
|
||||
int cx = display.getCursorX();
|
||||
int cy = display.getCursorY();
|
||||
// Default GFX font: cursor is top-left of cell (fontAscender=0), so cy=original_y.
|
||||
// Draw block at cy (not cy-8*sc — that formula assumes Lemon's baseline offset).
|
||||
display.fillRect(cx, cy, 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) {
|
||||
display_crc.update<int>(x);
|
||||
display_crc.update<int>(y);
|
||||
display_crc.update<int>(w);
|
||||
display_crc.update<int>(h);
|
||||
display.fillRect(x, y, w, h, _curr_color);
|
||||
}
|
||||
|
||||
void GxEPDDisplay::drawRect(int x, int y, int w, int h) {
|
||||
display_crc.update<int>(x);
|
||||
display_crc.update<int>(y);
|
||||
display_crc.update<int>(w);
|
||||
display_crc.update<int>(h);
|
||||
display.drawRect(x, y, w, h, _curr_color);
|
||||
if (scale() == 2 && 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) {
|
||||
display_crc.update<int>(x);
|
||||
display_crc.update<int>(y);
|
||||
display_crc.update<int>(w);
|
||||
display_crc.update<int>(h);
|
||||
display_crc.update<uintptr_t>((uintptr_t)bits);
|
||||
uint16_t widthInBytes = (w + 7) / 8;
|
||||
for (uint16_t by = 0; by < h; by++) {
|
||||
for (uint16_t bx = 0; bx < w; bx++) {
|
||||
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
|
||||
uint8_t bitMask = 0x80 >> (bx & 7);
|
||||
if (pgm_read_byte(bits + byteOffset) & bitMask) {
|
||||
display.drawPixel(x + bx, y + by, _curr_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t GxEPDDisplay::getTextWidth(const char* str) {
|
||||
if (_single_font && _text_sz == 1) {
|
||||
uint16_t total = 0;
|
||||
const int sc = scale();
|
||||
const uint8_t* p = (const uint8_t*)str;
|
||||
while (*p) total += glyphXAdvance(decodeCodepoint(p), sc);
|
||||
return total;
|
||||
}
|
||||
display.setTextWrap(false);
|
||||
int16_t x1, y1;
|
||||
uint16_t w, h;
|
||||
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
||||
display.setTextWrap(true);
|
||||
return w;
|
||||
forceRedraw();
|
||||
}
|
||||
|
||||
void GxEPDDisplay::setDisplayRotation(uint8_t rot) {
|
||||
display.setRotation(rot & 3);
|
||||
setDimensions(display.width(), display.height());
|
||||
last_display_crc_value = -1; // force redraw on next endFrame
|
||||
updateLayout();
|
||||
}
|
||||
|
||||
void GxEPDDisplay::endFrame() {
|
||||
uint32_t crc = display_crc.finalize();
|
||||
if (crc != last_display_crc_value) {
|
||||
if (frameChanged()) {
|
||||
bool partial = true;
|
||||
if (_full_refresh_interval > 0 && ++_partial_count >= _full_refresh_interval) {
|
||||
partial = false;
|
||||
@@ -301,6 +93,5 @@ void GxEPDDisplay::endFrame() {
|
||||
// is a separate job and stays with the periodic full refresh above.
|
||||
if (partial) display.writeInverseForRedrive();
|
||||
display.display(partial);
|
||||
last_display_crc_value = crc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,8 @@
|
||||
#include <GxEPD2_3C.h>
|
||||
#include <GxEPD2_4C.h>
|
||||
#include <GxEPD2_7C.h>
|
||||
#include <Fonts/FreeSans9pt7b.h>
|
||||
#include <Fonts/FreeSansBold12pt7b.h>
|
||||
#include <Fonts/FreeSans18pt7b.h>
|
||||
#include <CRC32.h>
|
||||
|
||||
#include "DisplayDriver.h"
|
||||
#include "MiscFixedFont.h"
|
||||
#include "EinkGfxDisplay.h"
|
||||
|
||||
// This driver calls callBusyPump() during its BUSY-pin waits; app code keys
|
||||
// its setBusyPumpFn() wiring off this so it isn't compiled for displays (or
|
||||
@@ -34,10 +29,12 @@
|
||||
#endif
|
||||
|
||||
// Panel native dimensions before rotation. Derived from the model class when
|
||||
// EINK_DISPLAY_MODEL is set; override with EINK_PANEL_W/H for other panels.
|
||||
// EINK_DISPLAY_MODEL is set (its visible width: some controllers carry a few
|
||||
// columns more than the glass shows); override with EINK_PANEL_W/H for other
|
||||
// panels.
|
||||
#if defined(EINK_DISPLAY_MODEL)
|
||||
#ifndef EINK_PANEL_W
|
||||
#define EINK_PANEL_W EINK_DISPLAY_MODEL::WIDTH
|
||||
#define EINK_PANEL_W EINK_DISPLAY_MODEL::WIDTH_VISIBLE
|
||||
#endif
|
||||
#ifndef EINK_PANEL_H
|
||||
#define EINK_PANEL_H EINK_DISPLAY_MODEL::HEIGHT
|
||||
@@ -53,26 +50,23 @@
|
||||
#define EINK_DISP_W ((DISPLAY_ROTATION & 1) ? EINK_PANEL_H : EINK_PANEL_W)
|
||||
#define EINK_DISP_H ((DISPLAY_ROTATION & 1) ? EINK_PANEL_W : EINK_PANEL_H)
|
||||
|
||||
class GxEPDDisplay : public DisplayDriver {
|
||||
// The panel object, as a base so it's built before EinkGfxDisplay draws on it.
|
||||
struct GxEPDPanel {
|
||||
#if defined(EINK_DISPLAY_MODEL)
|
||||
GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> display;
|
||||
GxEPDPanel() : display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {}
|
||||
#else
|
||||
GxEPD2_BW<GxEPD2_150_BN, 200> display;
|
||||
GxEPDPanel() : display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {}
|
||||
#endif
|
||||
};
|
||||
|
||||
class GxEPDDisplay : private GxEPDPanel, public EinkGfxDisplay {
|
||||
bool _init = false;
|
||||
bool _isOn = false;
|
||||
bool _single_font = 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;
|
||||
int _text_sz = 1;
|
||||
uint8_t _full_refresh_interval = 0;
|
||||
uint8_t _partial_count = 0;
|
||||
|
||||
int16_t drawGlyph(int16_t x, int16_t y, uint32_t cp, int sc);
|
||||
uint8_t glyphXAdvance(uint32_t cp, int sc);
|
||||
int scale() const { return (width() >= height()) ? 2 : 1; }
|
||||
|
||||
// GxEPD2_EPD::setBusyCallback() wants a plain function pointer with a
|
||||
// void* param, not a member function -- this trampolines back into the
|
||||
// instance so callBusyPump() (DisplayDriver.h) can reach whatever board
|
||||
@@ -86,78 +80,24 @@ class GxEPDDisplay : public DisplayDriver {
|
||||
}
|
||||
|
||||
public:
|
||||
#if defined(EINK_DISPLAY_MODEL)
|
||||
GxEPDDisplay() : DisplayDriver(EINK_DISP_W, EINK_DISP_H),
|
||||
display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {}
|
||||
#else
|
||||
GxEPDDisplay() : DisplayDriver(EINK_DISP_W, EINK_DISP_H),
|
||||
display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {}
|
||||
#endif
|
||||
GxEPDDisplay() : EinkGfxDisplay(EINK_DISP_W, EINK_DISP_H, display, GxEPD_BLACK, GxEPD_WHITE) {}
|
||||
|
||||
#ifdef ENABLE_SCREENSHOT
|
||||
const uint8_t* getBuffer() override { return display.getBuffer(); }
|
||||
uint16_t getBufferSize() override { return display.getBufferSize(); }
|
||||
uint8_t getDisplayType() override { return 1; } // 1 = e-ink
|
||||
// Return GxEPD2's own reported dimensions (uses WIDTH_VISIBLE, not the full physical WIDTH
|
||||
// stored in DisplayDriver). After setRotation(1): width()=HEIGHT, height()=WIDTH_VISIBLE.
|
||||
// GxEPD2's own dimensions after rotation, of the visible area.
|
||||
int screenshotWidth() override { return (int)display.width(); }
|
||||
int screenshotHeight() override { return (int)display.height(); }
|
||||
uint8_t screenshotRotation() override { return (uint8_t)display.getRotation(); }
|
||||
#endif
|
||||
|
||||
// Line height and approx. char width for each font size:
|
||||
// 1 = FreeSans9pt (lineH=16, charW≈9)
|
||||
// 2 = FreeSansBold12pt (lineH=20, charW≈12)
|
||||
// 3 = FreeSans18pt (lineH=28, charW≈17)
|
||||
// Built-in font scale used for size 4 (huge clock digits): 6×8 cell × 7 = 42×56.
|
||||
static const int BIG_TEXT_SCALE = 7;
|
||||
// Size 1 scales with orientation (portrait 1×, landscape 2×); size 2 is always 12×16;
|
||||
// size 3 is FreeSans18pt (~17×28); size 4 is the built-in font at 7× (~42×56),
|
||||
// used only for the stacked HH/MM clock on portrait e-ink. Landscape = width >= height.
|
||||
int getCharWidth() const override {
|
||||
int sc = scale();
|
||||
if (_text_sz == 4) return 6 * BIG_TEXT_SCALE;
|
||||
if (_text_sz == 3) return 17;
|
||||
if (_text_sz == 2) return 12 * sc;
|
||||
return 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 (_single_font ? 9 : 8) * sc; // misc-fixed 6x9 box height
|
||||
}
|
||||
void setSingleFont(bool) override { } // single-font: ignore toggles, stay misc-fixed 6x9
|
||||
bool isSingleFont() const override { return _single_font; }
|
||||
void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override {
|
||||
if (_single_font) {
|
||||
strncpy(dest, src, dest_size - 1);
|
||||
dest[dest_size - 1] = '\0';
|
||||
} else {
|
||||
translateUTF8Static(dest, src, dest_size);
|
||||
}
|
||||
}
|
||||
bool begin();
|
||||
|
||||
bool isOn() override { return _isOn; }
|
||||
bool isEink() override { return true; }
|
||||
void turnOn() override;
|
||||
void turnOff() override;
|
||||
void clear() override;
|
||||
void startFrame(Color bkg = DARK) override;
|
||||
void setTextSize(int sz) override;
|
||||
void setColor(Color c) override;
|
||||
void setCursor(int x, int y) override;
|
||||
void print(const char* str) override;
|
||||
void fillRect(int x, int y, int w, int h) override;
|
||||
void drawRect(int x, int y, int w, int h) override;
|
||||
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
|
||||
uint16_t getTextWidth(const char* str) override;
|
||||
uint16_t getCodepointWidth(uint32_t cp) override {
|
||||
if (_single_font && _text_sz == 1) return glyphXAdvance(cp, scale());
|
||||
return DisplayDriver::getCodepointWidth(cp);
|
||||
}
|
||||
void setDisplayRotation(uint8_t rot) override;
|
||||
void setFullRefreshInterval(uint8_t n) override { _full_refresh_interval = n; _partial_count = 0; }
|
||||
void endFrame() override;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,14 +5,17 @@
|
||||
#include "MiscFixedFont.h"
|
||||
#include "LemonIcons.h"
|
||||
|
||||
// Shared misc-fixed 6x9 text renderer for the monochrome OLED drivers.
|
||||
// Shared misc-fixed text renderer for the monochrome drivers (the OLEDs, the
|
||||
// e-ink panels and their simulator twins).
|
||||
//
|
||||
// Both Adafruit_SH110X and Adafruit_SSD1306 derive from Adafruit_GFX and the
|
||||
// font path is pure pixel plotting, so SH1106Display and SSD1306Display render
|
||||
// off this one implementation instead of two copies of it. Everything here
|
||||
// bypasses GFX's own print(): GFX walks bytes and would treat each byte of a
|
||||
// multi-byte UTF-8 sequence as its own CP437 glyph, and it can't reach the
|
||||
// custom UI icons either.
|
||||
// Each of them draws through an Adafruit_GFX and the font path is pure pixel
|
||||
// plotting, so they all render off this one implementation instead of a copy
|
||||
// each. Everything here bypasses GFX's own print(): GFX walks bytes and would
|
||||
// treat each byte of a multi-byte UTF-8 sequence as its own CP437 glyph, and
|
||||
// it can't reach the custom UI icons either.
|
||||
//
|
||||
// Every function takes the font, MiscFixed (6x9) unless said otherwise; any
|
||||
// misc-fixed size converted by tools/bdf2gfx.py works (MiscFixed8x13Font.h).
|
||||
//
|
||||
// Include only from a .cpp — the font tables are static const arrays, so a
|
||||
// header pulling this in would land a copy of them in every translation unit.
|
||||
@@ -20,18 +23,27 @@
|
||||
// `y` is the TOP of the text row in every function below, matching the UI's
|
||||
// coordinate convention (GFX fonts would use the baseline).
|
||||
|
||||
// Every misc-fixed size has a 2px descent, so the ascent is the rest of the row.
|
||||
static inline int miscFixedAscent(const GFXfont& f) { return f.yAdvance - 2; }
|
||||
// A space's advance: the cell width, for the substitution box too.
|
||||
static inline uint8_t miscFixedCellW(const GFXfont& f) {
|
||||
return pgm_read_byte(&f.glyph[' ' - f.first].xAdvance);
|
||||
}
|
||||
|
||||
// Pixel advance of one codepoint at text size sz. Unmapped codepoints get the
|
||||
// font's own 6px cell, same as the substitution box drawn for them.
|
||||
static inline uint8_t miscFixedXAdvance(uint32_t cp, int sz) {
|
||||
// font's own cell, same as the substitution box drawn for them.
|
||||
static inline uint8_t miscFixedXAdvance(uint32_t cp, int sz, const GFXfont& f = MiscFixed) {
|
||||
uint8_t xa;
|
||||
if (cp < MiscFixed.first || cp > MiscFixed.last) xa = 6;
|
||||
else xa = pgm_read_byte(&MiscFixedGlyphs[cp - MiscFixed.first].xAdvance);
|
||||
if (cp < f.first || cp > f.last) xa = miscFixedCellW(f);
|
||||
else xa = pgm_read_byte(&f.glyph[cp - f.first].xAdvance);
|
||||
return xa * sz;
|
||||
}
|
||||
|
||||
// Draw one codepoint at (x, y); returns the x to continue from.
|
||||
static inline int16_t miscFixedDrawGlyph(Adafruit_GFX& gfx, int16_t x, int16_t y,
|
||||
uint32_t cp, int sz, uint16_t color) {
|
||||
uint32_t cp, int sz, uint16_t color,
|
||||
const GFXfont& f = MiscFixed) {
|
||||
const int asc = miscFixedAscent(f);
|
||||
for (uint8_t i = 0; i < lemonIconCount; i++) {
|
||||
if (pgm_read_dword(&lemonIconCPs[i]) == cp) {
|
||||
const GFXglyph* g = &lemonIconGlyphs[i];
|
||||
@@ -40,14 +52,15 @@ static inline int16_t miscFixedDrawGlyph(Adafruit_GFX& gfx, int16_t x, int16_t y
|
||||
uint8_t xa = pgm_read_byte(&g->xAdvance);
|
||||
uint16_t bo = pgm_read_word(&g->bitmapOffset);
|
||||
uint8_t bits = 0, bit = 0;
|
||||
// The UI icons keep a baseline 1px above the font's, so they sit 1px
|
||||
// higher in their cell (+6 against the 6x9's ascent of 7).
|
||||
const int base = asc - 1;
|
||||
for (uint8_t row = 0; row < h; row++)
|
||||
for (uint8_t col = 0; col < w; col++) {
|
||||
if (!bit) { bits = pgm_read_byte(&lemonIconBitmaps[bo++]); bit = 0x80; }
|
||||
if (bits & bit) {
|
||||
// The UI icons keep a +6 baseline; the font glyphs below use +7
|
||||
// (misc-fixed's ascent), so the icons sit 1px higher in their cell.
|
||||
if (sz == 1) gfx.drawPixel(x + xo + col, y + 6 + yo + row, color);
|
||||
else gfx.fillRect(x + xo*sz + col*sz, y + 6*sz + yo*sz + row*sz, sz, sz, color);
|
||||
if (sz == 1) gfx.drawPixel(x + xo + col, y + base + yo + row, color);
|
||||
else gfx.fillRect(x + xo*sz + col*sz, y + base*sz + yo*sz + row*sz, sz, sz, color);
|
||||
}
|
||||
bit >>= 1;
|
||||
}
|
||||
@@ -55,15 +68,16 @@ static inline int16_t miscFixedDrawGlyph(Adafruit_GFX& gfx, int16_t x, int16_t y
|
||||
}
|
||||
}
|
||||
|
||||
if (cp < MiscFixed.first || cp > MiscFixed.last) {
|
||||
const int cw = miscFixedCellW(f);
|
||||
if (cp < f.first || cp > f.last) {
|
||||
// Substitution box for anything the font doesn't cover. Drawn at plain `y`
|
||||
// (not y - 7*sz): `y` is already the ascent top, so the box stays inside
|
||||
// (not y - asc*sz): `y` is already the ascent top, so the box stays inside
|
||||
// its own row instead of bleeding into the one above.
|
||||
if (cp >= 0x20) gfx.fillRect(x + sz, y, 4*sz, 6*sz, color);
|
||||
return x + 6 * sz;
|
||||
if (cp >= 0x20) gfx.fillRect(x + sz, y, (cw - 2)*sz, (asc - 1)*sz, color);
|
||||
return x + cw * sz;
|
||||
}
|
||||
|
||||
const GFXglyph* g = &MiscFixedGlyphs[cp - MiscFixed.first];
|
||||
const GFXglyph* g = &f.glyph[cp - f.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);
|
||||
@@ -71,10 +85,10 @@ static inline int16_t miscFixedDrawGlyph(Adafruit_GFX& gfx, int16_t x, int16_t y
|
||||
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(&MiscFixedBitmaps[bo++]); bit = 0x80; }
|
||||
if (!bit) { bits = pgm_read_byte(&f.bitmap[bo++]); bit = 0x80; }
|
||||
if (bits & bit) {
|
||||
if (sz == 1) gfx.drawPixel(x + xo + col, y + 7 + yo + row, color);
|
||||
else gfx.fillRect(x + xo*sz + col*sz, y + 7*sz + yo*sz + row*sz, sz, sz, color);
|
||||
if (sz == 1) gfx.drawPixel(x + xo + col, y + asc + yo + row, color);
|
||||
else gfx.fillRect(x + xo*sz + col*sz, y + asc*sz + yo*sz + row*sz, sz, sz, color);
|
||||
}
|
||||
bit >>= 1;
|
||||
}
|
||||
@@ -83,21 +97,22 @@ static inline int16_t miscFixedDrawGlyph(Adafruit_GFX& gfx, int16_t x, int16_t y
|
||||
|
||||
// Draw a UTF-8 string from the driver's current cursor, honouring '\n', and
|
||||
// leave the cursor where the text ended (same contract as GFX's print()).
|
||||
static inline void miscFixedPrint(Adafruit_GFX& gfx, const char* str, int sz, uint16_t color) {
|
||||
static inline void miscFixedPrint(Adafruit_GFX& gfx, const char* str, int sz, uint16_t color,
|
||||
const GFXfont& f = MiscFixed) {
|
||||
int16_t cx = gfx.getCursorX();
|
||||
int16_t cy = gfx.getCursorY();
|
||||
const uint8_t* p = (const uint8_t*)str;
|
||||
while (*p) {
|
||||
uint32_t cp = DisplayDriver::decodeCodepoint(p);
|
||||
if (cp == '\n') { cy += MiscFixed.yAdvance * sz; cx = 0; }
|
||||
else { cx = miscFixedDrawGlyph(gfx, cx, cy, cp, sz, color); }
|
||||
if (cp == '\n') { cy += f.yAdvance * sz; cx = 0; }
|
||||
else { cx = miscFixedDrawGlyph(gfx, cx, cy, cp, sz, color, f); }
|
||||
}
|
||||
gfx.setCursor(cx, cy);
|
||||
}
|
||||
|
||||
static inline uint16_t miscFixedTextWidth(const char* str, int sz) {
|
||||
static inline uint16_t miscFixedTextWidth(const char* str, int sz, const GFXfont& f = MiscFixed) {
|
||||
uint16_t width = 0;
|
||||
const uint8_t* p = (const uint8_t*)str;
|
||||
while (*p) width += miscFixedXAdvance(DisplayDriver::decodeCodepoint(p), sz);
|
||||
while (*p) width += miscFixedXAdvance(DisplayDriver::decodeCodepoint(p), sz, f);
|
||||
return width;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user