Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions examples/companion_radio/ui-new/UITask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -493,19 +493,26 @@ class MsgPreviewScreen : public UIScreen {
struct MsgEntry {
uint32_t timestamp;
char origin[62];
char msg[78];
char msg[MAX_TEXT_LEN + 1];
};
#define MAX_UNREAD_MSGS 32
#define MSG_SCROLL_INTERVAL_MS 2500
int num_unread;
int head = MAX_UNREAD_MSGS - 1; // index of latest unread message
MsgEntry unread[MAX_UNREAD_MSGS];
int msg_scroll_offset = 0;
unsigned long msg_last_scroll_ms = 0;
int msg_total_lines = 1;
int msg_visible_lines = 1;

public:
MsgPreviewScreen(UITask* task, mesh::RTCClock* rtc) : _task(task), _rtc(rtc) { num_unread = 0; }

void addPreview(uint8_t path_len, const char* from_name, const char* msg) {
head = (head + 1) % MAX_UNREAD_MSGS;
if (num_unread < MAX_UNREAD_MSGS) num_unread++;
msg_scroll_offset = 0;
msg_last_scroll_ms = millis();

auto p = &unread[head];
p->timestamp = _rtc->getCurrentTime();
Expand Down Expand Up @@ -550,7 +557,19 @@ class MsgPreviewScreen : public UIScreen {
display.setColor(UIColor::primary_txt);
char filtered_msg[sizeof(p->msg)];
display.translateUTF8ToBlocks(filtered_msg, p->msg, sizeof(filtered_msg));
display.printWordWrap(filtered_msg, display.width());

int visible_lines = 1;
int total_lines = display.printWordWrapScrolled(filtered_msg, display.width(),
display.height() - 25, msg_scroll_offset, &visible_lines);
msg_total_lines = total_lines;
msg_visible_lines = visible_lines;

unsigned long now = millis();
if (total_lines > visible_lines && now - msg_last_scroll_ms >= MSG_SCROLL_INTERVAL_MS) {
msg_last_scroll_ms = now;
msg_scroll_offset += visible_lines; // page forward a full screen at a time
if (msg_scroll_offset >= total_lines) msg_scroll_offset = 0; // loop back to the start
}

#if AUTO_OFF_MILLIS==0 // probably e-ink
return 10000; // 10 s
Expand All @@ -563,6 +582,8 @@ class MsgPreviewScreen : public UIScreen {
if (c == KEY_NEXT || c == KEY_RIGHT) {
head = (head + MAX_UNREAD_MSGS - 1) % MAX_UNREAD_MSGS;
num_unread--;
msg_scroll_offset = 0;
msg_last_scroll_ms = millis();
if (num_unread == 0) {
_task->gotoHomeScreen();
}
Expand All @@ -573,6 +594,22 @@ class MsgPreviewScreen : public UIScreen {
_task->gotoHomeScreen();
return true;
}
if (c == KEY_DOWN || c == KEY_UP) {
if (msg_total_lines > msg_visible_lines) {
if (c == KEY_DOWN) {
msg_scroll_offset += msg_visible_lines;
if (msg_scroll_offset >= msg_total_lines) msg_scroll_offset = 0;
} else {
msg_scroll_offset -= msg_visible_lines;
if (msg_scroll_offset < 0) {
// wrap to the last page
msg_scroll_offset = ((msg_total_lines - 1) / msg_visible_lines) * msg_visible_lines;
}
}
msg_last_scroll_ms = millis(); // manual scroll resets the auto-scroll timer
}
return true;
}
return false;
}
};
Expand Down Expand Up @@ -754,6 +791,18 @@ void UITask::loop() {
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_RIGHT);
}
ev = joystick_up.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_UP);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_UP);
}
ev = joystick_down.check();
if (ev == BUTTON_EVENT_CLICK) {
c = checkDisplayOn(KEY_DOWN);
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
c = handleLongPress(KEY_DOWN);
}
ev = back_btn.check();
if (ev == BUTTON_EVENT_TRIPLE_CLICK) {
c = handleTripleClick(KEY_SELECT);
Expand Down
29 changes: 28 additions & 1 deletion src/helpers/ui/DisplayDriver.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class DisplayDriver {

virtual bool isOn() = 0;
virtual bool isEink() { return false; } // default to non-eink, override in eink drivers
virtual bool supportsUTF8() { return false; } // true if print()/getTextWidth() can render non-ASCII UTF-8 directly
virtual void turnOn() = 0;
virtual void turnOff() = 0;
virtual void clear() = 0;
Expand All @@ -32,6 +33,20 @@ class DisplayDriver {
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

// Word-wraps str and draws only the lines that fall within [start_line,
// start_line + visible_lines) of the wrapped result, where visible_lines
// is however many lines fit in max_height (driver-specific font metrics).
// Returns the total number of wrapped lines, and if out_visible_lines is
// non-null, writes the visible-line capacity into it -- together these let
// a caller detect "there's more than fits" and page/scroll through it.
// Default falls back to plain printWordWrap() for drivers that don't
// support paging, reporting everything as fitting in one page.
virtual int printWordWrapScrolled(const char* str, int max_width, int max_height, int start_line, int* out_visible_lines = nullptr) {
printWordWrap(str, max_width);
if (out_visible_lines) *out_visible_lines = 1;
return 1;
}
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;
Expand All @@ -53,6 +68,13 @@ class DisplayDriver {

// convert UTF-8 characters to displayable block characters for compatibility
virtual void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) {
if (supportsUTF8()) { // driver can render UTF-8 directly, pass through unchanged
size_t len = strlen(src);
if (len >= dest_size) len = dest_size - 1;
memcpy(dest, src, len);
dest[len] = 0;
return;
}
size_t j = 0;
for (size_t i = 0; src[i] != 0 && j < dest_size - 1; i++) {
unsigned char c = (unsigned char)src[i];
Expand Down Expand Up @@ -97,7 +119,12 @@ class DisplayDriver {
int str_len = strlen(temp_str);

while (str_len > 0 && getTextWidth(temp_str) > max_width - ellipsis_width) {
temp_str[--str_len] = 0;
// drop one full UTF-8 character: continuation bytes (10xxxxxx) plus the lead byte
unsigned char b;
do {
b = (unsigned char)temp_str[--str_len];
temp_str[str_len] = 0;
} while (str_len > 0 && (b & 0xC0) == 0x80);
}
strcat(temp_str, ellipsis);

Expand Down
192 changes: 191 additions & 1 deletion src/helpers/ui/SH1106Display.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ ColorVal UIColor::popup_bkg = SH110X_BLACK;
ColorVal UIColor::popup_txt = SH110X_WHITE;
ColorVal UIColor::corp_blue = SH110X_WHITE;

#ifdef DISPLAY_UTF8_FONTS
static bool hasNonASCII(const char *str)
{
for (const char *p = str; *p; p++) {
if ((unsigned char)*p >= 0x80) return true;
}
return false;
}
#endif

bool SH1106Display::begin()
{
// Wire must already be initialised by board.begin() before this is called.
Expand All @@ -38,6 +48,9 @@ bool SH1106Display::begin()
// spi_dev NULL, and UITask::begin() calls turnOn() regardless of our
// return value, which then dereferences the null spi_dev.
bool ok = display.begin(addr ? addr : DISPLAY_ADDRESS, true);
#ifdef DISPLAY_UTF8_FONTS
_u8f.begin(display);
#endif
return addr != 0 && ok;
}

Expand All @@ -64,13 +77,26 @@ void SH1106Display::startFrame(ColorVal bkg)
display.clearDisplay(); // TODO: apply 'bkg'
_color = SH110X_WHITE;
display.setTextColor(_color);
display.setTextSize(1);
setTextSize(1);
display.cp437(true); // Use full 256 char 'Code Page 437' font
}

void SH1106Display::setTextSize(int sz)
{
display.setTextSize(sz);
#ifdef DISPLAY_UTF8_FONTS
// u8g2 only ships Cyrillic glyphs in a couple of fixed-size fonts; match
// the closest one to the scaled built-in 5x7 GFX font (6px/8px line at
// size 1, 12x16 at size 2+ -- 10x20 is the largest Cyrillic font available)
if (sz <= 1) {
_u8f.setFont(u8g2_font_6x12_t_cyrillic);
_fontHeight = 12;
} else {
_u8f.setFont(u8g2_font_10x20_t_cyrillic);
_fontHeight = 20;
}
_u8f.setFontMode(1); // must follow setFont(): setFont() resets to solid mode, which paints each glyph's background box
#endif
}

void SH1106Display::setColor(ColorVal c)
Expand All @@ -86,9 +112,168 @@ void SH1106Display::setCursor(int x, int y)

void SH1106Display::print(const char *str)
{
#ifdef DISPLAY_UTF8_FONTS
if (hasNonASCII(str)) {
printUTF8(str);
return;
}
#endif
display.print(str);
}

#ifdef DISPLAY_UTF8_FONTS
// Renders str through the U8g2 overlay one UTF-8 character at a time, so it
// can wrap at the right edge the same way Adafruit_GFX::print() does for the
// ASCII path. u8g2 draws from the text baseline while Adafruit_GFX's cursor
// is top-left, so the cursor is offset by the font ascent going in and out.
void SH1106Display::printUTF8(const char *str)
{
_u8f.setForegroundColor(_color);
int16_t ascent = _u8f.getFontAscent();
_u8f.setCursor(display.getCursorX(), display.getCursorY() + ascent);
int16_t line_height = ascent - _u8f.getFontDescent() + 1;

char glyph[5];
for (const char *p = str; *p; ) {
if (*p == '\n') {
_u8f.setCursor(0, _u8f.getCursorY() + line_height);
p++;
continue;
}
int n = 1;
while (n < 4 && (p[n] & 0xC0) == 0x80) n++; // include UTF-8 continuation bytes
memcpy(glyph, p, n);
glyph[n] = 0;

if (_u8f.getCursorX() + _u8f.getUTF8Width(glyph) > display.width()) {
_u8f.setCursor(0, _u8f.getCursorY() + line_height);
}
_u8f.print(glyph);
p += n;
}
display.setCursor(_u8f.getCursorX(), _u8f.getCursorY() - ascent);
}

// Greedy word wrap, measured and drawn entirely through the U8g2 overlay so
// wrap metrics stay consistent whether the text is ASCII, Cyrillic, or both.
// Splits on ASCII spaces only, which is UTF-8 safe since continuation bytes
// are always >= 0x80 and can never be mistaken for a space. A single word
// wider than max_width on its own is split mid-word at a clean UTF-8
// boundary instead of running off the edge.
//
// Only lines in [start_line, start_line + max_lines) are actually drawn, but
// every wrapped line is still counted -- the return value is always the
// *total* line count, letting callers (via printWordWrapScrolled) detect
// there's more content than fits and page/scroll through it.
int SH1106Display::wordWrapLines(const char *str, int max_width, int start_line, int max_lines)
{
char line[256];
size_t line_len = 0;
int x0 = display.getCursorX();
int16_t ascent = _u8f.getFontAscent();
int y = display.getCursorY() + ascent; // baseline for the first line
int line_index = 0;
const char *word_start = str;

_u8f.setForegroundColor(_color);

auto flush_line = [&]() {
if (line_len > 0) {
if (line_index >= start_line && line_index < start_line + max_lines) {
line[line_len] = 0;
_u8f.setCursor(x0, y);
_u8f.print(line);
y += _fontHeight;
}
line_index++;
line_len = 0;
}
};

while (true) {
const char *word_end = word_start;
while (*word_end && *word_end != ' ') word_end++;
size_t word_len = word_end - word_start;
if (word_len > sizeof(line) - 1) word_len = sizeof(line) - 1;

if (word_len > 0) {
char word_buf[256];
memcpy(word_buf, word_start, word_len);
word_buf[word_len] = 0;

if ((int)_u8f.getUTF8Width(word_buf) > max_width) {
// word alone is too wide: flush, then break it across as many
// fresh lines as needed
flush_line();
const char *wp = word_start;
size_t remaining = word_len;
while (remaining > 0) {
size_t take = remaining;
char chunk[256];
while (take > 0) {
memcpy(chunk, wp, take);
chunk[take] = 0;
if ((int)_u8f.getUTF8Width(chunk) <= max_width) break;
size_t shrink = take - 1;
while (shrink > 0 && (((unsigned char)wp[shrink]) & 0xC0) == 0x80) shrink--;
take = shrink;
}
if (take == 0) {
// even one character doesn't fit (degenerate max_width); force
// progress but keep it on a full UTF-8 boundary
take = 1;
while (take < remaining && (((unsigned char)wp[take]) & 0xC0) == 0x80) take++;
}

memcpy(line, wp, take);
line_len = take;
flush_line();

wp += take;
remaining -= take;
}
} else {
char candidate[256];
size_t candidate_len = line_len;
memcpy(candidate, line, line_len);
if (line_len > 0) candidate[candidate_len++] = ' ';
memcpy(candidate + candidate_len, word_start, word_len);
candidate_len += word_len;
candidate[candidate_len] = 0;

if (line_len > 0 && (int)_u8f.getUTF8Width(candidate) > max_width) {
flush_line();
memcpy(line, word_start, word_len);
line_len = word_len;
} else {
memcpy(line, candidate, candidate_len);
line_len = candidate_len;
}
}
}

if (*word_end == 0) break;
word_start = word_end + 1;
}

flush_line();
return line_index;
}

void SH1106Display::printWordWrap(const char *str, int max_width)
{
wordWrapLines(str, max_width, 0, 1000); // effectively unbounded line count
}

int SH1106Display::printWordWrapScrolled(const char *str, int max_width, int max_height, int start_line, int *out_visible_lines)
{
int max_lines = max_height / _fontHeight;
if (max_lines < 1) max_lines = 1;
if (out_visible_lines) *out_visible_lines = max_lines;
return wordWrapLines(str, max_width, start_line, max_lines);
}
#endif

void SH1106Display::fillRect(int x, int y, int w, int h)
{
display.fillRect(x, y, w, h, _color);
Expand All @@ -106,6 +291,11 @@ void SH1106Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h)

uint16_t SH1106Display::getTextWidth(const char *str)
{
#ifdef DISPLAY_UTF8_FONTS
if (hasNonASCII(str)) {
return _u8f.getUTF8Width(str);
}
#endif
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
Expand Down
Loading