20#include <goodisplay/gdey029T94.h>
25static const char*
TAG =
"MarkdownView";
41int charWidth(uint8_t fontId) {
43 if (!f || !f->glyph)
return 6;
44 int adv = f->glyph[0].xAdvance;
45 return adv > 0 ? adv : 6;
49int fontHeight(uint8_t fontId) {
56int ascentOffset(Gdey029T94* gfx, uint8_t fontId) {
59 int16_t x1 = 0, y1 = 0;
60 uint16_t w = 0, h = 0;
66void stripInline(
const char* text, uint16_t len,
char* out,
size_t outCap) {
68 for (uint16_t i = 0; i < len && o + 1 < outCap; ++i) {
69 const char c = text[i];
70 if (c ==
'*' || c ==
'_' || c ==
'`' || c ==
'\x10' || c ==
'\x11' || c ==
'\x12')
continue;
78void MarkdownView::addRow(
const char* text, uint16_t len, uint8_t font, uint8_t indent,
80 if (!rows_ || rowCount_ >=
MAX_ROWS) {
84 Row& r = rows_.get()[rowCount_++];
90 r.inverted = inverted;
105 const int cw = charWidth(line.
font);
106 int avail = textAreaWidth_ - line.
indent *
INDENT_PX - markerCols * cw;
107 int maxCols = avail / (cw > 0 ? cw : 6);
108 if (maxCols < 4) maxCols = 4;
113 while (start < line.
len) {
114 uint16_t remaining =
static_cast<uint16_t
>(line.
len - start);
115 uint16_t take = remaining <= maxCols ? remaining : static_cast<uint16_t>(maxCols);
116 if (take < remaining) {
119 while (brk > 0 && line.
text[start + brk] !=
' ') --brk;
120 if (brk > 0) take = brk;
124 start =
static_cast<uint16_t
>(start + take);
125 while (start < line.
len && line.
text[start] ==
' ') ++start;
137 void emit(
const StyledLine& l)
override { view->
appendParsed(l); }
143 strncpy(titleBuf_, title, MAX_TITLE_LEN - 1);
144 titleBuf_[MAX_TITLE_LEN - 1] =
'\0';
160 inlineLinkCount_ = 0;
162 checkSave_ =
nullptr;
163 checkSaveUd_ =
nullptr;
169 if (srcBuf_ && rows_) {
171 std::memcpy(srcBuf_.get(), src ? src :
"", n);
172 srcBuf_.get()[n] =
'\0';
173 rewriteInlineLinks();
174 size_t pn = std::strlen(srcBuf_.get());
178 if (r.truncated || len >=
MAX_SOURCE) truncated_ =
true;
183 LOG_D(
TAG,
"init: title='%s', rows=%u, trunc=%d", titleBuf_, rowCount_, truncated_);
188 strncpy(titleBuf_, title, MAX_TITLE_LEN - 1);
189 titleBuf_[MAX_TITLE_LEN - 1] =
'\0';
205 inlineLinkCount_ = 0;
207 checkSave_ =
nullptr;
208 checkSaveUd_ =
nullptr;
214 if (srcBuf_ && rows_) {
217 std::memcpy(srcBuf_.get(), src ? src :
"", n);
218 srcBuf_.get()[n] =
'\0';
221 int maxCols = textAreaWidth_ / (cw > 0 ? cw : 6);
222 if (maxCols < 4) maxCols = 4;
224 const char* base = srcBuf_.get();
228 while (nl < n && base[nl] !=
'\n') ++nl;
229 uint16_t lineLen =
static_cast<uint16_t
>(nl - off);
235 while (start < lineLen) {
236 uint16_t remaining =
static_cast<uint16_t
>(lineLen - start);
237 uint16_t take = remaining <= maxCols ? remaining : static_cast<uint16_t>(maxCols);
238 if (take < remaining) {
240 while (brk > 0 && base[off + start + brk] !=
' ') --brk;
241 if (brk > 0) take = brk;
245 start =
static_cast<uint16_t
>(start + take);
246 while (start < lineLen && base[off + start] ==
' ') ++start;
249 off = (nl < n) ? nl + 1 : nl;
254 LOG_D(
TAG,
"initPlain: title='%s', rows=%u, trunc=%d", titleBuf_, rowCount_, truncated_);
257uint16_t MarkdownView::visibleRowCount()
const {
270 inlineLinkCount_ = 0;
275 return ui::tr(
"core.hint_scroll_back");
281 if (scrollRow_ > 0) { scrollRow_--;
dirty_ =
true; }
284 if (scrollRow_ + 1 < rowCount_) { scrollRow_++;
dirty_ =
true; }
316 if (linkNum != 0 && rows_) {
318 std::snprintf(needle,
sizeof(needle),
"[%u]",
static_cast<unsigned>(linkNum));
319 size_t nl = std::strlen(needle);
320 for (uint16_t i = 0; i < rowCount_ && highlightRow_ < 0; ++i) {
321 const Row& r = rows_.get()[i];
322 if (r.len < nl)
continue;
323 for (uint16_t j = 0; j + nl <= r.len; ++j) {
324 if (std::memcmp(r.text + j, needle, nl) == 0) {
325 highlightRow_ =
static_cast<int>(i);
330 if (highlightRow_ >= 0) scrollRow_ =
static_cast<uint16_t
>(highlightRow_);
336 uint32_t& srcOff)
const {
337 const Row& r = rows_.get()[i];
341 srcOff = srcBuf_ ?
static_cast<uint32_t
>(r.text - srcBuf_.get()) : 0;
345 if (!interact_ || interactCount_ >= MAX_INTERACT)
return;
346 interact_.get()[interactCount_++] =
InteractRef{row, kind, id, srcOff};
350 if (interactSel_ < 0 || interactSel_ >=
static_cast<int>(interactCount_))
return nullptr;
351 return &interact_.get()[interactSel_];
356bool checkboxAt(
const char* t, uint16_t len) {
357 return len >= 3 && t[0] ==
'[' && t[2] ==
']' &&
358 (t[1] ==
' ' || t[1] ==
'x' || t[1] ==
'X');
362void MarkdownView::rewriteInlineLinks() {
363 inlineLinkCount_ = 0;
365 if (!srcBuf_)
return;
366 char* s = srcBuf_.get();
367 size_t rd = 0, wr = 0;
369 bool image = (s[rd] ==
'!' && s[rd + 1] ==
'[');
370 if (s[rd] ==
'[' || image) {
371 size_t tb = image ? rd + 2 : rd + 1;
373 while (s[te] && s[te] !=
']' && s[te] !=
'\n') ++te;
374 if (s[te] ==
']' && s[te + 1] ==
'(') {
375 size_t ub = te + 2, ue = ub;
376 while (s[ue] && s[ue] !=
')' && s[ue] !=
'\n') ++ue;
377 size_t urlLen = ue - ub;
378 if (s[ue] ==
')' && urlLen > 0) {
381 bool room = linkPool_ && linkUrlOff_ && inlineLinkCount_ < MAX_INTERACT &&
382 linkPoolLen_ + urlLen + 1 <= kLinkPoolCap;
384 linkUrlOff_.get()[inlineLinkCount_++] = linkPoolLen_;
385 std::memcpy(linkPool_.get() + linkPoolLen_, s + ub, urlLen);
386 linkPool_.get()[linkPoolLen_ + urlLen] =
'\0';
387 linkPoolLen_ =
static_cast<uint16_t
>(linkPoolLen_ + urlLen + 1);
388 s[wr++] = image ?
'\x12' :
'\x10';
389 for (
size_t k = tb; k < te; ++k) s[wr++] = s[k];
404 if (!rows_ || plain_)
return;
405 uint16_t ordinal = 0;
406 for (uint16_t i = 0; i < rowCount_; ++i) {
407 const Row& r = rows_.get()[i];
408 const uint32_t base = srcBuf_ ?
static_cast<uint32_t
>(r.text - srcBuf_.get()) : 0;
412 checkboxAt(r.text, r.len)) {
417 for (uint16_t j = 0; j < r.len; ++j) {
426 if (interactCount_ == 0)
return;
427 if (interactSel_ < 0) {
428 interactSel_ = (dir > 0) ? 0 : interactCount_ - 1;
430 interactSel_ = ((interactSel_ + dir) % interactCount_ + interactCount_) % interactCount_;
432 const InteractRef& it = interact_.get()[interactSel_];
433 highlightRow_ =
static_cast<int>(it.
row);
439 if (!srcBuf_ || srcOff == 0)
return false;
440 char* p = srcBuf_.get() + srcOff;
441 bool nowChecked = (*p ==
' ');
442 *p = nowChecked ?
'x' :
' ';
448 if (checkSave_ && srcBuf_) checkSave_(checkSaveUd_, srcBuf_.get(), std::strlen(srcBuf_.get()));
458 if (!linkPool_ || !linkUrlOff_ || it->
id >= inlineLinkCount_)
return;
459 const char* url = linkPool_.get() + linkUrlOff_.get()[it->
id];
468void MarkdownView::drawStyledRow(
void* gfxv,
const char* text, uint16_t len,
int x,
int yTop,
470 auto* gfx =
static_cast<Gdey029T94*
>(gfxv);
471 gfx->setFont(
nullptr);
472 bool bold =
false, strike =
false, link =
false;
474 for (uint16_t i = 0; i < len;) {
476 if (c ==
'\x10' || c ==
'\x12') { link =
true; ++i;
continue; }
477 if (c ==
'\x11') { link =
false; ++i;
continue; }
478 if (c ==
'`') { ++i;
continue; }
479 if (c ==
'*' || c ==
'_') {
480 if (i + 1 < len && text[i + 1] == c) { bold = !bold; i += 2; }
484 if (c ==
'~' && i + 1 < len && text[i + 1] ==
'~') { strike = !strike; i += 2;
continue; }
485 gfx->setCursor(cx, yTop);
486 gfx->write(
static_cast<uint8_t
>(c));
487 if (bold) { gfx->setCursor(cx + 1, yTop); gfx->write(
static_cast<uint8_t
>(c)); }
488 if (link) gfx->drawLine(cx, yTop + 7, cx + 5, yTop + 7, fg);
489 if (strike) gfx->drawLine(cx, yTop + 3, cx + 5, yTop + 3, fg);
498 auto* gfx =
static_cast<Gdey029T94*
>(
display->getNativeHandle());
501 const uint16_t width =
display->getWidth();
502 const uint16_t height =
display->getHeight();
505 gfx->fillScreen(EPD_WHITE);
507 gfx->fillRect(0, 0, width, height -
FOOTER_HEIGHT, EPD_WHITE);
510 gfx->setFont(
nullptr);
511 gfx->setTextColor(EPD_BLACK);
514 const char* title = (titleBuf_[0] !=
'\0') ? titleBuf_ :
nullptr;
522 for (uint16_t i = scrollRow_; i < rowCount_; ++i) {
523 const Row& r = rows_.get()[i];
540 const int h = fontHeight(r.font);
541 if (y + h > areaBottom)
break;
546 const bool inv = r.inverted || (
static_cast<int>(i) == highlightRow_);
548 gfx->fillRect(x, y, textAreaWidth_ - r.indent *
INDENT_PX, h, EPD_BLACK);
549 gfx->setTextColor(EPD_WHITE);
554 gfx->setFont(
nullptr);
555 gfx->setCursor(x, y);
556 gfx->print(
static_cast<char>(0x07));
559 gfx->setFont(
nullptr);
560 gfx->setCursor(x, y);
561 gfx->print(
static_cast<char>(0xB3));
567 drawStyledRow(gfx, r.text, r.len, x, y, inv ? EPD_WHITE : EPD_BLACK);
571 r.len <
sizeof(buf) - 1 ? r.len :
static_cast<uint16_t
>(
sizeof(buf) - 1);
572 std::memcpy(buf, r.text, cl);
575 stripInline(r.text, r.len, buf,
sizeof(buf));
579 gfx->setCursor(x, y + ascentOffset(gfx, r.font));
583 if (inv) gfx->setTextColor(EPD_BLACK);
585 gfx->setFont(
nullptr);
590 if (rowCount_ > shown || scrollRow_ > 0) {
594 rowCount_, shown ? shown : 1, scrollRow_);
598 const char* prefix =
nullptr;
600 prefix =
ui::tr(
"core.md_truncated");
601 }
else if (rowCount_ > shown) {
602 snprintf(posStr,
sizeof(posStr),
"%u/%u ", scrollRow_ + 1, rowCount_);
Optional HTML-rendering hook, decoupling HTML producers from consumers.
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
constexpr int FOOTER_HEIGHT
Footer bar height in pixels (used by drawFooterBar).
constexpr int SCROLL_INDICATOR_WIDTH
Scroll indicator column width in pixels.
static constexpr int TEXT_START_Y
static constexpr int TEXT_MARGIN
static constexpr int BLANK_HEIGHT
static constexpr int INDENT_PX
static constexpr int RULE_HEIGHT
static constexpr int TITLE_Y
Display layout constants.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_D(tag, fmt,...)
Scrollable viewer that renders a Markdown source.
void initPlain(const char *title, const char *src, size_t len)
Loads a source as plain, unrendered text (no Markdown parsing).
const char * getFooterHint() const override
void cycleInteractive(int dir)
Move the interactive selection by dir (+1/-1), highlighting its row.
void selectMarker(uint16_t linkNum)
Highlight the row containing the inline marker "[linkNum]" and scroll it into view....
void fireCheckSave()
Run the persistence callback with the current source (if set).
void addInteract(uint16_t row, Interact kind, uint16_t id, uint32_t srcOff)
static constexpr uint32_t MAX_SOURCE
Source byte cap.
void init(const char *title, const char *src, size_t len)
Loads and lays out a Markdown source.
virtual void rebuildInteractive()
Rebuild the interactive-element list from the rendered rows. Base finds task-list checkboxes; subclas...
void mdRowInfo(uint16_t i, const char *&text, uint16_t &len, bool &first, uint32_t &srcOff) const
void render(bool partial) override
InputResult onKey(char key) override
virtual void activateSelected()
Activate the selected element (Y). Base toggles checkboxes.
void appendParsed(const StyledLine &line)
Sink callback: wraps one parsed styled line into visual rows.
static constexpr uint16_t MAX_ROWS
Visual-row cap.
const InteractRef * selectedRef() const
InputResult onLongPress(char key) override
Interact
Interactive element kinds the view can select (4/6) and activate (Y).
bool flipCheckChar(uint32_t srcOff)
Toggle a checkbox state char in the source.
static ViewStack & instance()
Returns singleton view-stack instance.
void push(IView *view, void *context=nullptr)
PsramUniquePtr< T > psramAlloc(std::size_t count) noexcept
Allocate count elements of T in PSRAM (8-bit capable region).
IDisplay * getDisplayInstance()
Returns lazily created singleton display instance.
constexpr int SCROLL_INDICATOR_WIDTH
Scroll indicator column width in pixels.
constexpr int FOOTER_HEIGHT
Footer bar height in pixels (used by drawFooterBar).
void drawText(Adafruit_GFX *gfx, const char *text, const GFXfont *font)
Draws CP437-encoded text correctly for the given font: the built-in glcdfont (font == nullptr) is CP4...
void drawFooterBar(Gdey029T94 *gfx, uint16_t width, uint16_t height, const char *prefix, const char *hint, bool force=false)
Draws footer bar with optional prefix and hint text.
void measureText(Adafruit_GFX *gfx, const char *text, const GFXfont *font, int16_t x0, int16_t y0, int16_t *x1, int16_t *y1, uint16_t *w, uint16_t *h)
Measures CP437 text exactly as drawText would render it with font, so width-based layout (centering,...
void drawHeaderLeft(Gdey029T94 *gfx, const char *title, int x, int y, uint16_t width, int underlineOffset=18)
void drawScrollIndicator(Gdey029T94 *gfx, int x, int y, int listHeight, uint16_t totalItems, uint16_t visibleItems, uint16_t scrollPos)
Draws scroll arrows and scrollbar thumb.
Centralized key-code constants for cdc_views.
const char * tr(const char *key)
Look up a translation by string key.
const GFXfont * getGfxFont(FontId id)
Resolves a FontId to its underlying GFX font pointer.
MarkdownView * showMarkdown(const char *title, const char *src, size_t len)
Parses and shows a Markdown source on the view stack.
static constexpr char KEY_DOWN
Move selection down (numeric '8').
MdLineKind
Block role of a styled line, used by the view to apply layout/markers.
UrlOpenerFn urlOpener()
Current URL opener, or nullptr when none is registered.
static MarkdownView s_sharedMarkdownView
static constexpr char KEY_NO
Cancel / Back / Backspace.
MarkdownParseResult parseMarkdown(const char *src, size_t len, StyledLineSink &sink, size_t maxBytes)
Parses Markdown source into styled logical lines via a sink.
static constexpr int TITLE_Y
Layout constants mirror the ones used by T9InputView.
MarkdownView * showPlainText(const char *title, const char *src, size_t len)
Shows a source as plain, unrendered text on the view stack.
static constexpr char KEY_UP
Move selection up (numeric '2').
static constexpr char KEY_YES
Confirm / OK / Save.
ImageOpenerFn imageOpener()
Current image opener, or nullptr when none is registered.
static constexpr int TEXT_MARGIN
One selectable interactive element discovered in the rendered rows.
uint16_t id
Link: 1-based marker number; Check/Submit: ordinal.
uint16_t row
Visual row index (highlight/scroll target).
uint32_t srcOff
Check: byte offset of the state char in the source; else 0.
Sink the parser emits styled lines to (decouples model from storage).
One rendered logical line produced by the Markdown parser.
uint16_t len
Span length in bytes.
uint8_t font
One of mdfont.
uint8_t indent
Nesting depth (view multiplies by columns).
const char * text
Span into source (not null-terminated).
bool inverted
Inverse background (code).