CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
CanvasView.cpp
Go to the documentation of this file.
1
6
8#include "cdc_views/Fonts.h"
12#include "cdc_hal/IDisplay.h"
13#include "cdc_hal/IKeypad.h"
14#include "cdc_log.h"
15#include "esp_timer.h"
16#include <goodisplay/gdey029T94.h>
17#include <algorithm>
18#include <cstring>
19
20namespace cdc::ui {
21
22namespace {
23
24constexpr int TITLE_Y = 5;
25constexpr int HEADER_HEIGHT_DEFAULT = 30;
26
27const char* t9_chars(char key) {
28 switch (key) {
29 case '0': return " 0";
30 case '1': return ".?!,;:'\"()-_@#$%&*+=/\\<>[]{}|^~`1";
31 case '2': return "abc2";
32 case '3': return "def3";
33 case '4': return "ghi4";
34 case '5': return "jkl5";
35 case '6': return "mno6";
36 case '7': return "pqrs7";
37 case '8': return "tuv8";
38 case '9': return "wxyz9";
39 default: return nullptr;
40 }
41}
42
43uint32_t nowMs() {
44 return static_cast<uint32_t>(esp_timer_get_time() / 1000);
45}
46
47} // namespace
48
49Gdey029T94* CanvasView::gfx() const {
51 if (!display) return nullptr;
52 return static_cast<Gdey029T94*>(display->getNativeHandle());
53}
54
55int CanvasView::bodyBottom() const {
57 return display ? (display->getHeight() - cdc::ui::layout::FOOTER_HEIGHT) : 128;
58}
59
60void CanvasView::init(const char* title) {
61 title_ = title;
62 widgetCount_ = 0;
63 focused_ = 0;
64 textSize_ = 1;
65 textInverted_ = false;
66 keyCb_ = nullptr;
67 widgetCb_ = nullptr;
68 longPressCb_ = nullptr;
69 footer_ = nullptr;
70 headerHeight_ = (title && title[0] != '\0') ? HEADER_HEIGHT_DEFAULT : 0;
71 needsFullRefresh_ = true;
72 keyRepeatInitialMs_ = 0;
73 keyRepeatPeriodMs_ = 0;
74 headerDrawnOnce_ = false;
75 customFooter_ = nullptr;
76 cmdCount_ = 0;
77 textArenaUsed_ = 0;
78 overflowLogged_ = false;
79 fontId_ = 0;
80 dirty_ = true;
81}
82
83void CanvasView::setFooter(const char* hint) {
84 footer_ = hint;
85 customFooter_ = hint;
86}
87
88void CanvasView::setKeyRepeat(uint16_t initial_ms, uint16_t repeat_ms) {
89 keyRepeatInitialMs_ = initial_ms;
90 keyRepeatPeriodMs_ = repeat_ms;
91 applyKeypadConfig();
92}
93
94void CanvasView::getBodySize(uint16_t* w, uint16_t* h) const {
96 if (!display) {
97 if (w) *w = 0;
98 if (h) *h = 0;
99 return;
100 }
101 if (w) *w = display->getWidth();
102 if (h) *h = static_cast<uint16_t>(bodyBottom() - bodyTop());
103}
104
106 cmdCount_ = 0;
107 textArenaUsed_ = 0;
108}
109
110uint16_t CanvasView::internText(const char* text, uint16_t* outLen) {
111 size_t len = text ? strlen(text) : 0;
112 uint16_t off = textArenaUsed_;
113 if (off + len + 1 > TEXT_ARENA) {
114 // Truncate to the remaining arena (leave one byte for the NUL).
115 len = (off + 1 < TEXT_ARENA) ? (TEXT_ARENA - off - 1) : 0;
116 if (!overflowLogged_) {
117 LOG_W("CanvasView", "draw text arena full, truncating");
118 overflowLogged_ = true;
119 }
120 }
121 if (len) memcpy(&textArena_[off], text, len);
122 textArena_[off + len] = '\0';
123 textArenaUsed_ = static_cast<uint16_t>(off + len + 1);
124 *outLen = static_cast<uint16_t>(len);
125 return off;
126}
127
128void CanvasView::drawText(int16_t x, int16_t y, const char* text) {
129 if (!text || cmdCount_ >= MAX_CMDS) {
130 if (text && !overflowLogged_) {
131 LOG_W("CanvasView", "display list full, dropping draw");
132 overflowLogged_ = true;
133 }
134 return;
135 }
136 DrawCmd c{};
137 c.type = CmdType::Text;
138 c.x = x;
139 c.y = y;
140 c.fontId = fontId_;
141 c.textSize = textSize_;
142 c.inverted = textInverted_;
143 c.strOff = internText(text, &c.strLen);
144 cmds_[cmdCount_++] = c;
145}
146
147void CanvasView::drawTextAligned(int16_t x, int16_t y, int16_t w,
148 const char* text, uint8_t align) {
149 if (!text || cmdCount_ >= MAX_CMDS) {
150 if (text && !overflowLogged_) {
151 LOG_W("CanvasView", "display list full, dropping draw");
152 overflowLogged_ = true;
153 }
154 return;
155 }
156 DrawCmd c{};
157 c.type = CmdType::TextAligned;
158 c.x = x;
159 c.y = y;
160 c.w = w;
161 c.align = align;
162 c.fontId = fontId_;
163 c.textSize = textSize_;
164 c.inverted = textInverted_;
165 c.strOff = internText(text, &c.strLen);
166 cmds_[cmdCount_++] = c;
167}
168
169void CanvasView::drawRect(int16_t x, int16_t y, int16_t w, int16_t h, bool filled) {
170 if (cmdCount_ >= MAX_CMDS) return;
171 DrawCmd c{};
172 c.type = CmdType::Rect;
173 c.x = x;
174 c.y = y;
175 c.w = w;
176 c.h = h;
177 c.filled = filled;
178 cmds_[cmdCount_++] = c;
179}
180
181void CanvasView::invertRect(int16_t x, int16_t y, int16_t w, int16_t h) {
182 // No-op: the e-paper GFX backend has no pixel-readback primitive needed to
183 // invert an existing region.
184 (void)x; (void)y; (void)w; (void)h;
185}
186
187void CanvasView::drawHLine(int16_t x, int16_t y, int16_t w) {
188 if (cmdCount_ >= MAX_CMDS) return;
189 DrawCmd c{};
190 c.type = CmdType::HLine;
191 c.x = x;
192 c.y = y;
193 c.w = w;
194 cmds_[cmdCount_++] = c;
195}
196
197void CanvasView::drawVLine(int16_t x, int16_t y, int16_t h) {
198 if (cmdCount_ >= MAX_CMDS) return;
199 DrawCmd c{};
200 c.type = CmdType::VLine;
201 c.x = x;
202 c.y = y;
203 c.h = h;
204 cmds_[cmdCount_++] = c;
205}
206
207void CanvasView::paintText(int16_t x, int16_t y, int16_t w, const char* text,
208 uint8_t align, uint8_t fontId, uint8_t textSize,
209 bool inverted) {
210 auto* g = gfx();
211 if (!g || !text) return;
212 const GFXfont* font = getGfxFont(fontId);
213 g->setFont(font);
214 g->setTextSize(textSize);
215 g->setTextColor(inverted ? EPD_WHITE : EPD_BLACK);
216 g->setTextWrap(false);
217
218 int16_t draw_x = x;
219 if (align == 1 || align == 2) {
220 int16_t bx, by;
221 uint16_t bw, bh;
222 render::measureText(g, text, font, 0, 0, &bx, &by, &bw, &bh);
223 if (align == 1) {
224 draw_x = x + (w - static_cast<int16_t>(bw)) / 2;
225 } else {
226 draw_x = x + w - static_cast<int16_t>(bw);
227 }
228 }
229 g->setCursor(draw_x, y + bodyTop());
230 render::drawText(g, text, font);
231}
232
233void CanvasView::replayDisplayList() {
234 auto* g = gfx();
235 if (!g) return;
236 for (uint16_t i = 0; i < cmdCount_; ++i) {
237 const DrawCmd& c = cmds_[i];
238 switch (c.type) {
239 case CmdType::Text:
240 paintText(c.x, c.y, 0, &textArena_[c.strOff], 0,
241 c.fontId, c.textSize, c.inverted);
242 break;
243 case CmdType::TextAligned:
244 paintText(c.x, c.y, c.w, &textArena_[c.strOff], c.align,
245 c.fontId, c.textSize, c.inverted);
246 break;
247 case CmdType::Rect: {
248 int16_t yy = c.y + bodyTop();
249 if (c.filled) {
250 g->fillRect(c.x, yy, c.w, c.h, EPD_BLACK);
251 } else {
252 g->drawRect(c.x, yy, c.w, c.h, EPD_BLACK);
253 }
254 break;
255 }
256 case CmdType::HLine:
257 g->drawFastHLine(c.x, c.y + bodyTop(), c.w, EPD_BLACK);
258 break;
259 case CmdType::VLine:
260 g->drawFastVLine(c.x, c.y + bodyTop(), c.h, EPD_BLACK);
261 break;
262 }
263 }
264}
265
266void CanvasView::commit(bool full_refresh) {
267 if (full_refresh) {
268 needsFullRefresh_ = true;
269 }
270 markDirty();
271}
272
273CanvasView::Widget* CanvasView::findWidget(uint32_t id) {
274 if (id == 0) return nullptr;
275 for (uint8_t i = 0; i < widgetCount_; ++i) {
276 if (widgets_[i].id == id) return &widgets_[i];
277 }
278 return nullptr;
279}
280
281const CanvasView::Widget* CanvasView::findWidget(uint32_t id) const {
282 if (id == 0) return nullptr;
283 for (uint8_t i = 0; i < widgetCount_; ++i) {
284 if (widgets_[i].id == id) return &widgets_[i];
285 }
286 return nullptr;
287}
288
289CanvasView::Widget* CanvasView::focusedWidget() {
290 return findWidget(focused_);
291}
292
293bool CanvasView::addSlider(uint32_t id, int32_t min, int32_t max,
294 int32_t initial, int32_t step) {
295 if (id == 0 || widgetCount_ >= MAX_WIDGETS || findWidget(id)) {
296 return false;
297 }
298 Widget& w = widgets_[widgetCount_++];
299 w = Widget{};
300 w.id = id;
301 w.type = WidgetType::Slider;
302 w.min = min;
303 w.max = max;
304 w.step = step > 0 ? step : 1;
305 w.value = std::clamp(initial, min, max);
306 return true;
307}
308
309bool CanvasView::addText(uint32_t id, uint16_t max_len, const char* initial) {
310 if (id == 0 || widgetCount_ >= MAX_WIDGETS || findWidget(id)) {
311 return false;
312 }
313 Widget& w = widgets_[widgetCount_++];
314 w = Widget{};
315 w.id = id;
316 w.type = WidgetType::Text;
317 w.max_len = max_len < MAX_TEXT_LEN ? max_len : (MAX_TEXT_LEN - 1);
318 if (initial) {
319 size_t n = std::min(strlen(initial), static_cast<size_t>(w.max_len));
320 memcpy(w.text, initial, n);
321 w.text[n] = '\0';
322 w.text_len = static_cast<uint16_t>(n);
323 }
324 return true;
325}
326
327bool CanvasView::addButton(uint32_t id) {
328 if (id == 0 || widgetCount_ >= MAX_WIDGETS || findWidget(id)) {
329 return false;
330 }
331 Widget& w = widgets_[widgetCount_++];
332 w = Widget{};
333 w.id = id;
334 w.type = WidgetType::Button;
335 return true;
336}
337
338bool CanvasView::removeWidget(uint32_t id) {
339 for (uint8_t i = 0; i < widgetCount_; ++i) {
340 if (widgets_[i].id == id) {
341 for (uint8_t j = i + 1; j < widgetCount_; ++j) {
342 widgets_[j - 1] = widgets_[j];
343 }
344 --widgetCount_;
345 widgets_[widgetCount_] = Widget{};
346 if (focused_ == id) focused_ = 0;
347 return true;
348 }
349 }
350 return false;
351}
352
353bool CanvasView::setValue(uint32_t id, int32_t value) {
354 Widget* w = findWidget(id);
355 if (!w || w->type != WidgetType::Slider) return false;
356 w->value = std::clamp(value, w->min, w->max);
357 return true;
358}
359
360bool CanvasView::getValue(uint32_t id, int32_t* out) const {
361 const Widget* w = findWidget(id);
362 if (!w || !out) return false;
363 *out = w->value;
364 return true;
365}
366
367bool CanvasView::setText(uint32_t id, const char* text) {
368 Widget* w = findWidget(id);
369 if (!w || w->type != WidgetType::Text) return false;
370 size_t n = std::min(text ? strlen(text) : size_t{0},
371 static_cast<size_t>(w->max_len));
372 if (text) memcpy(w->text, text, n);
373 w->text[n] = '\0';
374 w->text_len = static_cast<uint16_t>(n);
375 w->t9_last_key = 0;
376 w->t9_press_count = 0;
377 return true;
378}
379
380int CanvasView::getText(uint32_t id, char* out, size_t cap) const {
381 const Widget* w = findWidget(id);
382 if (!w || !out || cap == 0) return -1;
383 size_t n = std::min(static_cast<size_t>(w->text_len), cap - 1);
384 memcpy(out, w->text, n);
385 out[n] = '\0';
386 return static_cast<int>(n);
387}
388
389bool CanvasView::setFocus(uint32_t id) {
390 if (id == 0) {
391 focused_ = 0;
392 return true;
393 }
394 Widget* w = findWidget(id);
395 if (!w) return false;
396 focused_ = id;
397 return true;
398}
399
400void CanvasView::t9_commit_pending(Widget& w) {
401 w.t9_last_key = 0;
402 w.t9_press_count = 0;
403}
404
405void CanvasView::t9_apply_key(Widget& w, char key, uint32_t now) {
406 const char* table = t9_chars(key);
407 if (!table) return;
408 size_t table_len = strlen(table);
409 if (table_len == 0) return;
410
411 bool same_key = (w.t9_last_key == key)
412 && ((now - w.t9_last_time) < T9_SETTLE_MS)
413 && w.text_len > 0;
414
415 if (same_key) {
416 w.t9_press_count = (w.t9_press_count + 1) % static_cast<uint8_t>(table_len);
417 w.text[w.text_len - 1] = table[w.t9_press_count];
418 } else {
419 if (w.text_len >= w.max_len) {
420 return;
421 }
422 w.text[w.text_len++] = table[0];
423 w.text[w.text_len] = '\0';
424 w.t9_press_count = 0;
425 }
426 w.t9_last_key = key;
427 w.t9_last_time = now;
428}
429
430InputResult CanvasView::dispatchKeyToWidget(Widget& w, char key) {
431 uint32_t now = nowMs();
432 switch (w.type) {
433 case WidgetType::Slider: {
434 if (key == '4' || key == KEY_UP) {
435 int32_t nv = std::clamp<int32_t>(w.value - w.step, w.min, w.max);
436 if (nv != w.value) {
437 w.value = nv;
438 if (widgetCb_) widgetCb_(w.id, WidgetEvent::Changed);
439 }
441 }
442 if (key == '6' || key == KEY_DOWN) {
443 int32_t nv = std::clamp<int32_t>(w.value + w.step, w.min, w.max);
444 if (nv != w.value) {
445 w.value = nv;
446 if (widgetCb_) widgetCb_(w.id, WidgetEvent::Changed);
447 }
449 }
450 if (key == KEY_YES) {
451 if (widgetCb_) widgetCb_(w.id, WidgetEvent::Committed);
453 }
454 if (key == KEY_NO) {
455 if (widgetCb_) widgetCb_(w.id, WidgetEvent::Cancelled);
457 }
459 }
460 case WidgetType::Text: {
461 if (key >= '0' && key <= '9') {
462 t9_apply_key(w, key, now);
463 if (widgetCb_) widgetCb_(w.id, WidgetEvent::Changed);
465 }
466 if (key == '*') {
467 t9_commit_pending(w);
468 if (w.text_len > 0) {
469 --w.text_len;
470 w.text[w.text_len] = '\0';
471 if (widgetCb_) widgetCb_(w.id, WidgetEvent::Changed);
472 }
474 }
475 if (key == '#') {
476 t9_commit_pending(w);
477 if (w.text_len < w.max_len) {
478 w.text[w.text_len++] = ' ';
479 w.text[w.text_len] = '\0';
480 if (widgetCb_) widgetCb_(w.id, WidgetEvent::Changed);
481 }
483 }
484 if (key == KEY_YES) {
485 t9_commit_pending(w);
486 if (widgetCb_) widgetCb_(w.id, WidgetEvent::Committed);
488 }
489 if (key == KEY_NO) {
490 if (w.text_len > 0 && w.t9_last_key != 0
491 && (now - w.t9_last_time) < T9_SETTLE_MS) {
492 t9_commit_pending(w);
494 }
495 if (w.text_len > 0) {
496 --w.text_len;
497 w.text[w.text_len] = '\0';
498 if (widgetCb_) widgetCb_(w.id, WidgetEvent::Changed);
500 }
501 if (widgetCb_) widgetCb_(w.id, WidgetEvent::Cancelled);
503 }
505 }
506 case WidgetType::Button: {
507 if (key == KEY_YES) {
508 if (widgetCb_) widgetCb_(w.id, WidgetEvent::Committed);
510 }
511 if (key == KEY_NO) {
512 if (widgetCb_) widgetCb_(w.id, WidgetEvent::Cancelled);
514 }
516 }
517 default:
519 }
520}
521
523 Widget* focused = focusedWidget();
524 if (focused) {
525 InputResult r = dispatchKeyToWidget(*focused, key);
526 if (r == InputResult::CONSUMED) {
528 }
529 }
530
531 if (keyCb_) {
532 keyCb_(key, focused_);
534 }
535
536 if (key == KEY_NO) {
538 }
540}
541
542// Push this canvas's keypad modes (deferred short-press while a long-press
543// handler is registered, and key-repeat) to the global keypad. Applied while
544// the canvas is the active view; onExit restores the defaults for other views.
545void CanvasView::applyKeypadConfig() {
546 if (auto* kp = hal::getKeypadInstance()) {
547 kp->setDeferShortPress(hal::IKeypad::DEFER_SRC_VIEW, longPressCb_ != nullptr);
548 kp->setKeyRepeat(keyRepeatInitialMs_, keyRepeatPeriodMs_);
549 }
550}
551
553 longPressCb_ = cb;
554 applyKeypadConfig();
555}
556
558 if (longPressCb_) {
559 longPressCb_(key);
561 }
563}
564
565void CanvasView::onEnter(void* context) {
566 ViewBase::onEnter(context);
567 applyKeypadConfig();
568}
569
572 applyKeypadConfig();
573}
574
576 if (auto* kp = hal::getKeypadInstance()) {
577 kp->setDeferShortPress(hal::IKeypad::DEFER_SRC_VIEW, false);
578 kp->setKeyRepeat(0, 0);
579 }
581}
582
583void CanvasView::render(bool partial) {
585 if (!display) return;
586 auto* g = gfx();
587 if (!g) return;
588
589 const uint16_t width = display->getWidth();
590 const uint16_t height = display->getHeight();
591
592 if (!partial || needsFullRefresh_) {
593 g->fillScreen(EPD_WHITE);
594 needsFullRefresh_ = false;
595 headerDrawnOnce_ = false;
596 }
597
598 if (title_ && title_[0] != '\0' && !headerDrawnOnce_) {
599 g->setTextColor(EPD_BLACK);
600 g->setTextSize(1);
601 g->setTextWrap(false);
603 headerDrawnOnce_ = true;
604 }
605
606 replayDisplayList();
607
608 const char* hint = customFooter_ ? customFooter_ : footer_;
609 render::drawFooterBar(g, width, height, nullptr, hint, hint != nullptr);
610
611 dirty_ = false;
612}
613
614} // namespace cdc::ui
static const char * t9_chars[]
T9 digit-to-character mapping table.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
static constexpr uint32_t DEFER_SRC_VIEW
active view (e.g. canvas long-press)
Definition IKeypad.h:90
void render(bool partial) override
void getBodySize(uint16_t *w, uint16_t *h) const
bool removeWidget(uint32_t id)
InputResult onLongPress(char key) override
int getText(uint32_t id, char *out, size_t cap) const
void drawText(int16_t x, int16_t y, const char *text)
void(*)(char key) LongPressCallback
Definition CanvasView.h:48
void drawHLine(int16_t x, int16_t y, int16_t w)
void init(const char *title)
InputResult onKey(char key) override
void commit(bool full_refresh)
void onResume() override
static constexpr uint16_t MAX_CMDS
Definition CanvasView.h:43
void onExit() override
void setLongPressCallback(LongPressCallback cb)
void onEnter(void *context) override
static constexpr uint8_t MAX_WIDGETS
Definition CanvasView.h:37
bool setFocus(uint32_t id)
bool setValue(uint32_t id, int32_t value)
bool addText(uint32_t id, uint16_t max_len, const char *initial)
bool getValue(uint32_t id, int32_t *out) const
bool addButton(uint32_t id)
bool setText(uint32_t id, const char *text)
static constexpr uint16_t T9_SETTLE_MS
Definition CanvasView.h:39
static constexpr uint16_t MAX_TEXT_LEN
Definition CanvasView.h:38
void invertRect(int16_t x, int16_t y, int16_t w, int16_t h)
void setKeyRepeat(uint16_t initial_ms, uint16_t repeat_ms)
bool addSlider(uint32_t id, int32_t min, int32_t max, int32_t initial, int32_t step)
void setFooter(const char *hint)
static constexpr uint16_t TEXT_ARENA
Definition CanvasView.h:44
void drawTextAligned(int16_t x, int16_t y, int16_t w, const char *text, uint8_t align)
void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, bool filled)
void drawVLine(int16_t x, int16_t y, int16_t h)
void onResume() override
Definition IView.h:169
void markDirty() override
Definition IView.h:186
const char * title_
Definition IView.h:202
void onExit() override
Definition IView.h:167
void onEnter(void *context) override
Definition IView.h:162
const char * customFooter_
Definition IView.h:203
IDisplay * getDisplayInstance()
Returns lazily created singleton display instance.
IKeypad * getKeypadInstance()
Returns the singleton keypad service instance.
constexpr int FOOTER_HEIGHT
Footer bar height in pixels (used by drawFooterBar).
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 drawText(Gdey029T94 *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 drawHeaderLeft(Gdey029T94 *gfx, const char *title, int x, int y, uint16_t width, int underlineOffset=18)
void measureText(Gdey029T94 *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,...
Centralized key-code constants for cdc_views.
Definition IModule.h:8
const GFXfont * getGfxFont(FontId id)
Resolves a FontId to its underlying GFX font pointer.
Definition Fonts.cpp:26
static constexpr char KEY_DOWN
Move selection down (numeric '8').
Definition KeyCodes.h:35
Gdey029T94 * display
InputResult
Definition IView.h:10
static constexpr char KEY_NO
Cancel / Back / Backspace.
Definition KeyCodes.h:44
static constexpr int TITLE_Y
Layout constants mirror the ones used by T9InputView.
static constexpr char KEY_UP
Move selection up (numeric '2').
Definition KeyCodes.h:32
static constexpr char KEY_YES
Confirm / OK / Save.
Definition KeyCodes.h:41