18#include <goodisplay/gdey029T94.h>
28constexpr int HEADER_HEIGHT_DEFAULT = 30;
29constexpr uint16_t BLOB_NONE = 0xFFFF;
33 case '0':
return " 0";
34 case '1':
return ".?!,;:'\"()-_@#$%&*+=/\\<>[]{}|^~`1";
35 case '2':
return "abc2";
36 case '3':
return "def3";
37 case '4':
return "ghi4";
38 case '5':
return "jkl5";
39 case '6':
return "mno6";
40 case '7':
return "pqrs7";
41 case '8':
return "tuv8";
42 case '9':
return "wxyz9";
43 default:
return nullptr;
48 return static_cast<uint32_t
>(esp_timer_get_time() / 1000);
52constexpr uint8_t ANIM_DONE_CAP =
57Gdey029T94* CanvasView::gfx()
const {
60 return static_cast<Gdey029T94*
>(
display->getNativeHandle());
63int CanvasView::bodyBottom()
const {
78 textInverted_ =
false;
81 longPressCb_ =
nullptr;
83 headerHeight_ = (title && title[0] !=
'\0') ? HEADER_HEIGHT_DEFAULT : 0;
84 needsFullRefresh_ =
true;
85 keyRepeatInitialMs_ = 0;
86 keyRepeatPeriodMs_ = 0;
87 headerDrawnOnce_ =
false;
94 memset(elems_, 0,
sizeof(elems_));
95 overflowLogged_ =
false;
100 if (sprites_.hasArena()) sprites_.setArena(spriteArena_.get(),
SPRITE_ARENA);
105 idleCleanupAtMs_ = 0;
107 animCommitCount_ = 0;
118 keyRepeatInitialMs_ = initial_ms;
119 keyRepeatPeriodMs_ = repeat_ms;
130 if (w) *w =
display->getWidth();
131 if (h) *h =
static_cast<uint16_t
>(bodyBottom() - bodyTop());
141 memset(elems_, 0,
sizeof(elems_));
150 }
else if (sprites_.hasArena()) {
154 idleCleanupAtMs_ = 0;
157void CanvasView::pushCmd(DrawCmd& c) {
159 cmds_[cmdCount_++] = c;
162void CanvasView::allocArenas() {
163 if (cmds_ && textArena_ && widgets_ && blobArena_) {
164 memset(cmds_.get(), 0,
sizeof(DrawCmd) *
MAX_CMDS);
165 memset(widgets_.get(), 0,
sizeof(Widget) *
MAX_WIDGETS);
172 if (!cmds_ || !textArena_ || !widgets_ || !blobArena_) {
173 LOG_E(
"CanvasView",
"PSRAM arena allocation failed");
180 memset(cmds_.get(), 0,
sizeof(DrawCmd) *
MAX_CMDS);
181 memset(widgets_.get(), 0,
sizeof(Widget) *
MAX_WIDGETS);
184uint16_t CanvasView::internBlob(
const uint8_t* data, uint16_t len) {
185 if (!blobArena_ || !data || len == 0)
return BLOB_NONE;
186 uint16_t off = blobUsed_;
188 if (!overflowLogged_) {
189 LOG_W(
"CanvasView",
"draw blob arena full, dropping bitmap");
190 overflowLogged_ =
true;
194 memcpy(&blobArena_[off], data, len);
195 blobUsed_ =
static_cast<uint16_t
>(off + len);
199uint16_t CanvasView::internText(
const char* text, uint16_t* outLen) {
201 if (outLen) *outLen = 0;
204 size_t len = text ? strlen(text) : 0;
205 uint16_t off = textArenaUsed_;
209 if (!overflowLogged_) {
210 LOG_W(
"CanvasView",
"draw text arena full, truncating");
211 overflowLogged_ =
true;
214 if (len) memcpy(&textArena_[off], text, len);
215 textArena_[off + len] =
'\0';
216 textArenaUsed_ =
static_cast<uint16_t
>(off + len + 1);
217 *outLen =
static_cast<uint16_t
>(len);
223 if (!text || !cmds_ || cmdCount_ >=
MAX_CMDS) {
224 if (text && !overflowLogged_) {
225 LOG_W(
"CanvasView",
"display list full, dropping draw");
226 overflowLogged_ =
true;
231 c.type = CmdType::Text;
235 c.textSize = textSize_;
236 c.inverted = textInverted_;
237 c.strOff = internText(text, &c.strLen);
242 const char* text, uint8_t align) {
244 if (!text || !cmds_ || cmdCount_ >=
MAX_CMDS) {
245 if (text && !overflowLogged_) {
246 LOG_W(
"CanvasView",
"display list full, dropping draw");
247 overflowLogged_ =
true;
252 c.type = CmdType::TextAligned;
258 c.textSize = textSize_;
259 c.inverted = textInverted_;
260 c.strOff = internText(text, &c.strLen);
266 if (!cmds_ || cmdCount_ >=
MAX_CMDS)
return;
268 c.type = CmdType::Rect;
275 c.inverted = inkWhite_;
281 if (!cmds_ || cmdCount_ >=
MAX_CMDS)
return;
283 c.type = CmdType::Pixel;
286 c.inverted = inkWhite_;
292 if (!cmds_ || cmdCount_ >=
MAX_CMDS)
return;
294 c.type = CmdType::Line;
299 c.inverted = inkWhite_;
305 if (!cmds_ || cmdCount_ >=
MAX_CMDS)
return;
307 c.type = CmdType::Circle;
313 c.inverted = inkWhite_;
318 int16_t x2, int16_t y2,
bool filled) {
320 if (!cmds_ || cmdCount_ >=
MAX_CMDS)
return;
322 c.type = CmdType::Triangle;
331 c.inverted = inkWhite_;
336 int16_t r,
bool filled) {
338 if (!cmds_ || cmdCount_ >=
MAX_CMDS)
return;
340 c.type = CmdType::RoundRect;
347 c.inverted = inkWhite_;
352 const uint8_t* data, uint32_t len) {
354 if (!cmds_ || cmdCount_ >=
MAX_CMDS || !data || w <= 0 || h <= 0)
return;
355 uint32_t need =
static_cast<uint32_t
>((w + 7) / 8) *
static_cast<uint32_t
>(h);
356 if (len < need || need == 0 || need >
BLOB_ARENA) {
357 if (!overflowLogged_) {
358 LOG_W(
"CanvasView",
"bitmap too large or short, dropping");
359 overflowLogged_ =
true;
363 uint16_t off = internBlob(data,
static_cast<uint16_t
>(need));
364 if (off == BLOB_NONE)
return;
366 c.type = CmdType::Bitmap;
372 c.strLen =
static_cast<uint16_t
>(need);
378 if (!cmds_ || cmdCount_ >=
MAX_CMDS)
return;
380 c.type = CmdType::HLine;
384 c.inverted = inkWhite_;
390 if (!cmds_ || cmdCount_ >=
MAX_CMDS)
return;
392 c.type = CmdType::VLine;
396 c.inverted = inkWhite_;
400void CanvasView::paintText(int16_t x, int16_t y, int16_t w,
const char* text,
401 uint8_t align, uint8_t fontId, uint8_t textSize,
404 if (!g || !text)
return;
407 g->setTextSize(textSize);
408 g->setTextColor(inverted ? EPD_WHITE : EPD_BLACK);
409 g->setTextWrap(
false);
412 if (align == 1 || align == 2) {
417 draw_x = x + (w -
static_cast<int16_t
>(bw)) / 2;
419 draw_x = x + w -
static_cast<int16_t
>(bw);
422 g->setCursor(draw_x, y + bodyTop());
426CanvasView::Elem* CanvasView::findElem(uint32_t
id) {
427 if (
id == 0)
return nullptr;
428 for (uint8_t i = 0; i < elemCount_; ++i) {
429 if (elems_[i].
id ==
id)
return &elems_[i];
434const CanvasView::Elem* CanvasView::findElem(uint32_t
id)
const {
435 return const_cast<CanvasView*
>(
this)->findElem(
id);
440 if (
id == 0)
return false;
443 if (!overflowLogged_) {
444 LOG_W(
"CanvasView",
"element table full, dropping element");
445 overflowLogged_ =
true;
449 elems_[elemCount_] = Elem{};
450 elems_[elemCount_].id = id;
464 Elem* e = findElem(
id);
465 if (!e)
return false;
473 Elem* e = findElem(
id);
474 if (!e)
return false;
475 e->ox =
static_cast<int16_t
>(e->ox + dx);
476 e->oy =
static_cast<int16_t
>(e->oy + dy);
482 Elem* e = findElem(
id);
483 if (!e)
return false;
484 e->hidden = !visible;
489bool CanvasView::dropElemCmds(uint32_t
id) {
491 for (uint16_t i = 0; i < cmdCount_; ++i) {
492 if (cmds_[i].elemId !=
id) {
493 if (out != i) cmds_[out] = cmds_[i];
497 bool dropped = out != cmdCount_;
499 if (dropped) compactArenas();
505 Elem* e = findElem(
id);
506 if (!e)
return false;
509 animator_.cancelForElem(
id);
511 uint8_t idx =
static_cast<uint8_t
>(e - elems_);
512 for (uint8_t i = idx + 1; i < elemCount_; ++i) {
513 elems_[i - 1] = elems_[i];
516 elems_[elemCount_] = Elem{};
517 if (curElem_ ==
id) curElem_ = 0;
523 if (!findElem(
id))
return false;
530 Elem* e = findElem(
id);
531 if (!e)
return false;
538 const Elem* e = findElem(
id);
539 if (!e)
return false;
546 uint16_t* w, uint16_t* h) {
548 Elem* e = findElem(
id);
549 if (!e || !cmds_)
return false;
551 int32_t minX = INT32_MAX, minY = INT32_MAX;
552 int32_t maxX = INT32_MIN, maxY = INT32_MIN;
554 for (uint16_t i = 0; i < cmdCount_; ++i) {
555 if (cmds_[i].elemId !=
id)
continue;
558 if (!cmdBounds(cmds_[i], &cx, &cy, &cw, &ch))
continue;
560 minX = std::min<int32_t>(minX, cx);
561 minY = std::min<int32_t>(minY, cy);
562 maxX = std::max<int32_t>(maxX, cx + cw);
563 maxY = std::max<int32_t>(maxY, cy + ch);
565 if (!any)
return false;
566 if (x) *x =
static_cast<int16_t
>(minX + e->ox);
567 if (y) *y =
static_cast<int16_t
>(minY + e->oy);
568 if (w) *w =
static_cast<uint16_t
>(maxX - minX);
569 if (h) *h =
static_cast<uint16_t
>(maxY - minY);
574bool CanvasView::cmdBounds(
const DrawCmd& c, int16_t* x, int16_t* y,
575 uint16_t* w, uint16_t* h) {
576 int32_t x0 = c.x, y0 = c.y, x1 = c.x, y1 = c.y;
579 case CmdType::TextAligned: {
581 if (!g)
return false;
583 g->setTextSize(c.textSize);
588 int16_t draw_x = c.x;
590 draw_x =
static_cast<int16_t
>(c.x + (c.w -
static_cast<int16_t
>(bw)) / 2);
591 }
else if (c.align == 2) {
592 draw_x =
static_cast<int16_t
>(c.x + c.w -
static_cast<int16_t
>(bw));
602 case CmdType::RoundRect:
619 x0 = std::min(c.x, c.w);
620 y0 = std::min(c.y, c.h);
621 x1 = std::max(c.x, c.w) + 1;
622 y1 = std::max(c.y, c.h) + 1;
624 case CmdType::Circle:
630 case CmdType::Triangle:
631 x0 = std::min(c.x, std::min(c.w, c.x2));
632 y0 = std::min(c.y, std::min(c.h, c.y2));
633 x1 = std::max(c.x, std::max(c.w, c.x2)) + 1;
634 y1 = std::max(c.y, std::max(c.h, c.y2)) + 1;
636 case CmdType::Bitmap:
640 case CmdType::Sprite: {
641 anim::SpriteStore::FrameView fv;
642 if (!sprites_.frameView(c.strOff, &fv))
return false;
643 uint16_t bw = fv.scrollWindow != 0 ? fv.scrollWindow
647 x1 = c.x + bw * fv.scale;
648 y1 = c.y + bh * fv.scale;
652 *x =
static_cast<int16_t
>(x0);
653 *y =
static_cast<int16_t
>(y0);
654 *w =
static_cast<uint16_t
>(x1 - x0);
655 *h =
static_cast<uint16_t
>(y1 - y0);
662void CanvasView::compactArenas() {
663 uint16_t textCursor = 0;
664 uint16_t blobCursor = 0;
665 for (uint16_t i = 0; i < cmdCount_; ++i) {
666 DrawCmd& c = cmds_[i];
667 if (c.type == CmdType::Text || c.type == CmdType::TextAligned) {
668 uint16_t span =
static_cast<uint16_t
>(c.strLen + 1);
669 if (c.strOff != textCursor) {
670 memmove(&textArena_[textCursor], &textArena_[c.strOff], span);
671 c.strOff = textCursor;
673 textCursor =
static_cast<uint16_t
>(textCursor + span);
674 }
else if (c.type == CmdType::Bitmap && c.strOff != BLOB_NONE) {
675 if (c.strOff != blobCursor) {
676 memmove(&blobArena_[blobCursor], &blobArena_[c.strOff], c.strLen);
677 c.strOff = blobCursor;
679 blobCursor =
static_cast<uint16_t
>(blobCursor + c.strLen);
682 textArenaUsed_ = textCursor;
683 blobUsed_ = blobCursor;
689void CanvasView::applyElemOffset(DrawCmd& c, int16_t ox, int16_t oy)
const {
690 c.x =
static_cast<int16_t
>(c.x + ox);
691 c.y =
static_cast<int16_t
>(c.y + oy);
694 c.w =
static_cast<int16_t
>(c.w + ox);
695 c.h =
static_cast<int16_t
>(c.h + oy);
697 case CmdType::Triangle:
698 c.w =
static_cast<int16_t
>(c.w + ox);
699 c.h =
static_cast<int16_t
>(c.h + oy);
700 c.x2 =
static_cast<int16_t
>(c.x2 + ox);
701 c.y2 =
static_cast<int16_t
>(c.y2 + oy);
711void CanvasView::replayDisplayList() {
712 cdc::core::RecursiveMutexGuard guard(editMutex_);
713 if (!gfx() || !cmds_)
return;
716 uint8_t layerCount = 0;
717 layers[layerCount++] = 0;
718 for (uint8_t i = 0; i < elemCount_; ++i) {
719 int16_t z = elems_[i].z;
721 for (uint8_t j = 0; j < layerCount; ++j) {
722 if (layers[j] == z) {
727 if (!seen) layers[layerCount++] = z;
730 for (uint8_t i = 1; i < layerCount; ++i) {
731 int16_t v = layers[i];
732 int8_t j =
static_cast<int8_t
>(i - 1);
733 while (j >= 0 && layers[j] > v) {
734 layers[j + 1] = layers[j];
740 for (uint8_t l = 0; l < layerCount; ++l) {
741 for (uint16_t i = 0; i < cmdCount_; ++i) {
742 DrawCmd c = cmds_[i];
745 Elem* e = findElem(c.elemId);
747 if (e->hidden)
continue;
749 if (e->ox != 0 || e->oy != 0) applyElemOffset(c, e->ox, e->oy);
752 if (z != layers[l])
continue;
758void CanvasView::replayCmd(
const DrawCmd& c) {
762 const uint16_t ink = c.inverted ? EPD_WHITE : EPD_BLACK;
766 paintText(c.x, c.y, 0, &textArena_[c.strOff], 0,
767 c.fontId, c.textSize, c.inverted);
769 case CmdType::TextAligned:
770 paintText(c.x, c.y, c.w, &textArena_[c.strOff], c.align,
771 c.fontId, c.textSize, c.inverted);
773 case CmdType::Rect: {
774 int16_t yy = c.y + bodyTop();
776 if (c.shade >= 255) g->fillRect(c.x, yy, c.w, c.h, ink);
779 g->drawRect(c.x, yy, c.w, c.h, ink);
784 g->drawFastHLine(c.x, c.y + bodyTop(), c.w, ink);
787 g->drawFastVLine(c.x, c.y + bodyTop(), c.h, ink);
790 g->drawPixel(c.x, c.y + bodyTop(), ink);
793 g->drawLine(c.x, c.y + bodyTop(), c.w, c.h + bodyTop(), ink);
795 case CmdType::Circle:
797 if (c.shade >= 255) g->fillCircle(c.x, c.y + bodyTop(), c.w, ink);
800 g->drawCircle(c.x, c.y + bodyTop(), c.w, ink);
803 case CmdType::Triangle:
806 g->fillTriangle(c.x, c.y + bodyTop(), c.w, c.h + bodyTop(),
807 c.x2, c.y2 + bodyTop(), ink);
808 else if (c.shade > 0)
810 c.x2, c.y2 + bodyTop(), c.shade, ink);
812 g->drawTriangle(c.x, c.y + bodyTop(), c.w, c.h + bodyTop(),
813 c.x2, c.y2 + bodyTop(), ink);
816 case CmdType::RoundRect:
818 g->fillRoundRect(c.x, c.y + bodyTop(), c.w, c.h, c.x2, ink);
820 g->drawRoundRect(c.x, c.y + bodyTop(), c.w, c.h, c.x2, ink);
823 case CmdType::Bitmap:
824 g->drawBitmap(c.x, c.y + bodyTop(), &blobArena_[c.strOff],
827 case CmdType::Sprite: {
828 anim::SpriteStore::FrameView fv;
829 if (!sprites_.frameView(c.strOff, &fv))
break;
830 render::BlitOpts opts;
836 opts.
scale = fv.scale;
837 if (fv.scrollWindow != 0) {
838 opts.
srcX = fv.scrollX;
839 opts.
srcW = fv.scrollWindow;
840 opts.
srcSpan =
static_cast<uint16_t
>(fv.w + fv.scrollGap);
843 g, c.x,
static_cast<int16_t
>(c.y + bodyTop()), fv.data,
844 static_cast<int16_t
>(fv.w),
static_cast<int16_t
>(fv.h),
845 opts, EPD_BLACK, EPD_WHITE);
854 needsFullRefresh_ =
true;
861bool CanvasView::ensureSpriteArena() {
862 if (sprites_.
hasArena())
return true;
865 LOG_E(
"CanvasView",
"sprite arena allocation failed");
873 const uint8_t* data, uint32_t len) {
875 if (!ensureSpriteArena())
return -1;
876 return sprites_.create(w, h, frame_count, data, len);
881 return sprites_.setMask(handle, mask, len);
886 return sprites_.setFlags(handle,
flags);
891 return sprites_.setScale(handle,
scale);
898 const char* text, uint16_t step_px, uint16_t frame_ms) {
901 if (!g || !text || window_w <= 0 || step_px == 0)
return 0;
902 if (!cmds_ || cmdCount_ >=
MAX_CMDS)
return -1;
903 if (!ensureSpriteArena())
return -1;
906 g->setTextSize(textSize_);
910 if (bw == 0 || bh == 0)
return 0;
912 if (bw > 1024) bw = 1024;
914 MonoSurface strip(bw,
static_cast<uint16_t
>(bh + 2));
915 if (!strip.
ok())
return -1;
917 strip.setTextSize(textSize_);
918 strip.setTextColor(1);
919 strip.setTextWrap(
false);
920 strip.setCursor(
static_cast<int16_t
>(-bx),
static_cast<int16_t
>(-by));
923 int32_t handle = sprites_.create(bw,
static_cast<uint16_t
>(bh + 2), 1,
925 if (handle <= 0)
return handle;
926 sprites_.scroll(
static_cast<uint32_t
>(handle),
static_cast<uint16_t
>(window_w),
927 step_px,
static_cast<uint16_t
>(window_w), frame_ms, nowMs());
930 c.type = CmdType::Sprite;
933 c.strOff =
static_cast<uint16_t
>(handle);
937 idleCleanupAtMs_ = 0;
944 return sprites_.setFrame(handle, frame);
949 return sprites_.frame(handle);
955 return sprites_.setFrameDurations(handle, ms, count);
959 uint16_t repeat, uint32_t done_action_id) {
961 if (!sprites_.play(handle, mode, frame_ms, repeat, done_action_id, nowMs())) {
965 idleCleanupAtMs_ = 0;
972 return sprites_.stop(handle);
977 return sprites_.destroy(handle);
983 return sprites_.frameView(handle, out);
988 if (!cmds_ || cmdCount_ >=
MAX_CMDS)
return;
990 if (!sprites_.frameView(handle, &fv))
return;
992 c.type = CmdType::Sprite;
995 c.strOff =
static_cast<uint16_t
>(handle);
1003 if (!findElem(cfg.
elemId))
return 0;
1004 uint32_t handle = animator_.start(cfg, nowMs());
1007 idleCleanupAtMs_ = 0;
1014 uint16_t count, uint32_t done_action_id) {
1016 if (!findElem(elem_id))
return 0;
1017 uint32_t handle = animator_.blink(elem_id, period_ms, count,
1018 done_action_id, nowMs());
1021 idleCleanupAtMs_ = 0;
1028 return animator_.cancel(handle);
1033 return animator_.pause(handle, paused, nowMs());
1038 return animator_.state(handle);
1043 uint8_t n = animator_.activeCount();
1045 return sprites_.anyPlaying() ?
static_cast<uint8_t
>(n + 1) : n;
1051 animPolicy_ = policy;
1053 animFrameIntervalMs_ =
static_cast<uint16_t
>(1000 / fps);
1059bool CanvasView::getElemOffset(uint32_t elemId, int16_t* ox, int16_t* oy) {
1060 Elem* e = findElem(elemId);
1061 if (!e)
return false;
1067bool CanvasView::setElemOffset(uint32_t elemId, int16_t ox, int16_t oy) {
1068 Elem* e = findElem(elemId);
1069 if (!e)
return false;
1075bool CanvasView::setElemVisible(uint32_t elemId,
bool visible) {
1076 Elem* e = findElem(elemId);
1077 if (!e)
return false;
1078 e->hidden = !visible;
1082CanvasView::Widget* CanvasView::findWidget(uint32_t
id) {
1083 if (
id == 0)
return nullptr;
1084 for (uint8_t i = 0; i < widgetCount_; ++i) {
1085 if (widgets_[i].
id ==
id)
return &widgets_[i];
1090const CanvasView::Widget* CanvasView::findWidget(uint32_t
id)
const {
1091 if (
id == 0)
return nullptr;
1092 for (uint8_t i = 0; i < widgetCount_; ++i) {
1093 if (widgets_[i].
id ==
id)
return &widgets_[i];
1098CanvasView::Widget* CanvasView::focusedWidget() {
1099 return findWidget(focused_);
1103 int32_t initial, int32_t step) {
1104 if (
id == 0 || !widgets_ || widgetCount_ >=
MAX_WIDGETS || findWidget(
id)) {
1107 Widget& w = widgets_[widgetCount_++];
1113 w.step = step > 0 ? step : 1;
1114 w.value = std::clamp(initial, min, max);
1119 if (
id == 0 || !widgets_ || widgetCount_ >=
MAX_WIDGETS || findWidget(
id)) {
1122 Widget& w = widgets_[widgetCount_++];
1128 size_t n = std::min(strlen(initial),
static_cast<size_t>(w.max_len));
1129 memcpy(w.text, initial, n);
1131 w.text_len =
static_cast<uint16_t
>(n);
1137 if (
id == 0 || !widgets_ || widgetCount_ >=
MAX_WIDGETS || findWidget(
id)) {
1140 Widget& w = widgets_[widgetCount_++];
1148 for (uint8_t i = 0; i < widgetCount_; ++i) {
1149 if (widgets_[i].
id ==
id) {
1150 for (uint8_t j = i + 1; j < widgetCount_; ++j) {
1151 widgets_[j - 1] = widgets_[j];
1154 widgets_[widgetCount_] = Widget{};
1155 if (focused_ ==
id) focused_ = 0;
1163 Widget* w = findWidget(
id);
1165 w->value = std::clamp(value, w->min, w->max);
1170 const Widget* w = findWidget(
id);
1171 if (!w || !out)
return false;
1177 Widget* w = findWidget(
id);
1179 size_t n = std::min(text ? strlen(text) :
size_t{0},
1180 static_cast<size_t>(w->max_len));
1181 if (text) memcpy(w->text, text, n);
1183 w->text_len =
static_cast<uint16_t
>(n);
1185 w->t9_press_count = 0;
1190 const Widget* w = findWidget(
id);
1191 if (!w || !out || cap == 0)
return -1;
1192 size_t n = std::min(
static_cast<size_t>(w->text_len), cap - 1);
1193 memcpy(out, w->text, n);
1195 return static_cast<int>(n);
1203 Widget* w = findWidget(
id);
1204 if (!w)
return false;
1209void CanvasView::t9_commit_pending(Widget& w) {
1211 w.t9_press_count = 0;
1214void CanvasView::t9_apply_key(Widget& w,
char key, uint32_t now) {
1217 size_t table_len = strlen(table);
1218 if (table_len == 0)
return;
1220 bool same_key = (w.t9_last_key == key)
1225 w.t9_press_count = (w.t9_press_count + 1) %
static_cast<uint8_t
>(table_len);
1226 w.text[w.text_len - 1] = table[w.t9_press_count];
1228 if (w.text_len >= w.max_len) {
1231 w.text[w.text_len++] = table[0];
1232 w.text[w.text_len] =
'\0';
1233 w.t9_press_count = 0;
1235 w.t9_last_key = key;
1236 w.t9_last_time = now;
1239InputResult CanvasView::dispatchKeyToWidget(Widget& w,
char key) {
1240 uint32_t now = nowMs();
1243 if (key ==
'4' || key ==
KEY_UP) {
1244 int32_t nv = std::clamp<int32_t>(w.value - w.step, w.min, w.max);
1245 if (nv != w.value) {
1251 if (key ==
'6' || key ==
KEY_DOWN) {
1252 int32_t nv = std::clamp<int32_t>(w.value + w.step, w.min, w.max);
1253 if (nv != w.value) {
1270 if (key >=
'0' && key <=
'9') {
1271 t9_apply_key(w, key, now);
1276 t9_commit_pending(w);
1277 if (w.text_len > 0) {
1279 w.text[w.text_len] =
'\0';
1285 t9_commit_pending(w);
1286 if (w.text_len < w.max_len) {
1287 w.text[w.text_len++] =
' ';
1288 w.text[w.text_len] =
'\0';
1294 t9_commit_pending(w);
1299 if (w.text_len > 0 && w.t9_last_key != 0
1301 t9_commit_pending(w);
1304 if (w.text_len > 0) {
1306 w.text[w.text_len] =
'\0';
1332 Widget* focused = focusedWidget();
1334 InputResult r = dispatchKeyToWidget(*focused, key);
1341 keyCb_(key, focused_);
1354void CanvasView::applyKeypadConfig() {
1357 kp->setKeyRepeat(keyRepeatInitialMs_, keyRepeatPeriodMs_);
1363 applyKeypadConfig();
1376 applyKeypadConfig();
1381 applyKeypadConfig();
1384 if (pausedAtMs_ != 0) {
1385 uint32_t delta = nowMs() - pausedAtMs_;
1388 animator_.shiftTime(delta);
1389 sprites_.shiftTime(delta);
1390 if (idleCleanupAtMs_ != 0) idleCleanupAtMs_ += delta;
1395 pausedAtMs_ = nowMs();
1402 kp->setKeyRepeat(0, 0);
1409 if (sprites_.hasArena()) sprites_.setArena(spriteArena_.get(),
SPRITE_ARENA);
1410 animActive_ =
false;
1411 idleCleanupAtMs_ = 0;
1421 if (pausedAtMs_ != 0)
return;
1424 uint8_t doneCount = 0;
1425 bool cleanup =
false;
1429 bool wasActive = animator_.activeCount() > 0 || sprites_.anyPlaying();
1431 &&
static_cast<uint32_t
>(nowMs - lastAnimStepMs_) >= animFrameIntervalMs_) {
1432 lastAnimStepMs_ = nowMs;
1433 bool changed =
false;
1435 changed |= animator_.advance(nowMs, *
this, done,
1439 changed |= sprites_.advance(nowMs, done + doneCount,
1440 static_cast<uint8_t
>(ANIM_DONE_CAP - doneCount),
1442 doneCount =
static_cast<uint8_t
>(doneCount + n);
1449 animCommitCount_ = 0;
1454 bool nowActive = animator_.activeCount() > 0 || sprites_.anyPlaying();
1458 if (nowActive) idleCleanupAtMs_ = 0;
1459 animActive_ = nowActive;
1462 if (idleCleanupAtMs_ != 0
1463 &&
static_cast<int32_t
>(nowMs - idleCleanupAtMs_) >= 0) {
1464 idleCleanupAtMs_ = 0;
1469 for (uint8_t i = 0; i < doneCount; ++i) {
1470 if (animCb_) animCb_(done[i].doneActionId, done[i].handle, done[i].refId);
1483 const uint16_t width =
display->getWidth();
1484 const uint16_t height =
display->getHeight();
1486 if (!partial || needsFullRefresh_) {
1487 g->fillScreen(EPD_WHITE);
1488 needsFullRefresh_ =
false;
1489 headerDrawnOnce_ =
false;
1492 if (
title_ &&
title_[0] !=
'\0' && !headerDrawnOnce_) {
1493 g->setTextColor(EPD_BLACK);
1495 g->setTextWrap(
false);
1497 headerDrawnOnce_ =
true;
1500 replayDisplayList();
Offscreen 1-bpp Adafruit_GFX render target in PSRAM.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
#define LOG_E(tag, fmt,...)
Scoped guard for a FreeRTOS recursive mutex.
static constexpr uint32_t DEFER_SRC_VIEW
active view (e.g. canvas long-press)
bool elemShow(uint32_t id, bool visible)
Show or hide the element (hidden elements are skipped on replay).
void render(bool partial) override
uint32_t animStart(const anim::TweenConfig &cfg)
void getBodySize(uint16_t *w, uint16_t *h) const
bool removeWidget(uint32_t id)
void endElem()
Stop tagging draw calls with an element id.
bool spriteSetScale(uint32_t handle, uint8_t scale)
Integer upscale 1..4 applied whenever the sprite is drawn.
static constexpr uint8_t ANIM_FPS_DEFAULT
bool elemGetBounds(uint32_t id, int16_t *x, int16_t *y, uint16_t *w, uint16_t *h)
InputResult onLongPress(char key) override
int getText(uint32_t id, char *out, size_t cap) const
uint32_t animBlink(uint32_t elem_id, uint16_t period_ms, uint16_t count, uint32_t done_action_id)
@ ANIM_REFRESH_LIGHT
PARTIAL_LIGHT always, no automatic cleanup.
@ ANIM_REFRESH_AUTO
PARTIAL_LIGHT while running + FAST cleanup.
void drawText(int16_t x, int16_t y, const char *text)
void(*)(char key) LongPressCallback
void drawHLine(int16_t x, int16_t y, int16_t w)
void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1)
static constexpr uint8_t ANIM_FPS_MAX
void clearBody(bool keep_sprites=false)
bool beginElem(uint32_t id)
void drawRoundRect(int16_t x, int16_t y, int16_t w, int16_t h, int16_t r, bool filled)
void init(const char *title)
InputResult onKey(char key) override
bool elemClear(uint32_t id)
void commit(bool full_refresh)
bool spriteFrameView(uint32_t handle, anim::SpriteStore::FrameView *out) const
bool spriteDestroy(uint32_t handle)
static constexpr uint16_t MAX_CMDS
static constexpr uint8_t ANIM_HYGIENE_COMMITS
Endless animations get one hygiene FAST every this many anim commits.
bool spriteSetFlags(uint32_t handle, uint8_t flags)
void setLongPressCallback(LongPressCallback cb)
void onEnter(void *context) override
bool animPause(uint32_t handle, bool paused)
bool spriteStop(uint32_t handle)
static constexpr uint16_t ANIM_CLEANUP_IDLE_MS
Idle time after the last animation before the AUTO ghost-cleanup FAST.
uint8_t animActiveCount() const
Live tweens plus playing sprites.
static constexpr uint16_t BLOB_ARENA
Pixel-data arena for recorded bitmaps. 8 KB holds one full-body mono bitmap.
void drawTriangle(int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool filled)
bool spriteSetMask(uint32_t handle, const uint8_t *mask, uint32_t len)
static constexpr uint8_t MAX_WIDGETS
bool setFocus(uint32_t id)
bool spritePlay(uint32_t handle, uint8_t mode, uint16_t frame_ms, uint16_t repeat, uint32_t done_action_id)
bool spriteSetFrameDurations(uint32_t handle, const uint16_t *ms, uint16_t count)
int8_t animState(uint32_t handle) const
void drawBitmap(int16_t x, int16_t y, int16_t w, int16_t h, const uint8_t *data, uint32_t len)
bool animCancel(uint32_t handle)
Handle 0 cancels all (no events).
bool setValue(uint32_t id, int32_t value)
bool addText(uint32_t id, uint16_t max_len, const char *initial)
bool elemSetZ(uint32_t id, int8_t z)
static constexpr uint32_t SPRITE_ARENA
Pixel arena for sprite frame sheets (lazy PSRAM allocation).
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
bool elemRemove(uint32_t id)
bool elemMove(uint32_t id, int16_t dx, int16_t dy)
Shift the element's replay offset by a delta.
int32_t spriteGetFrame(uint32_t handle) const
static constexpr uint16_t MAX_TEXT_LEN
void setKeyRepeat(uint16_t initial_ms, uint16_t repeat_ms)
int32_t marquee(int16_t x, int16_t y, int16_t window_w, const char *text, uint16_t step_px, uint16_t frame_ms)
bool spriteSetFrame(uint32_t handle, uint16_t frame)
void drawCircle(int16_t x, int16_t y, int16_t r, bool filled)
bool elemSetOffset(uint32_t id, int16_t ox, int16_t oy)
Set the element's replay offset relative to its recorded coordinates.
bool addSlider(uint32_t id, int32_t min, int32_t max, int32_t initial, int32_t step)
bool setAnimPolicy(uint8_t policy, uint8_t max_fps)
Configure refresh policy and step-rate cap (0 = default 4 fps).
void drawPixel(int16_t x, int16_t y)
void setFooter(const char *hint)
static constexpr uint16_t TEXT_ARENA
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)
bool elemGetOffset(uint32_t id, int16_t *ox, int16_t *oy) const
Read the element's current replay offset.
void drawSprite(int16_t x, int16_t y, uint32_t handle)
Record a draw-by-reference of the sprite's current frame at (x, y).
int32_t spriteCreate(uint16_t w, uint16_t h, uint16_t frame_count, const uint8_t *data, uint32_t len)
void onTick(uint32_t nowMs) override
static constexpr uint8_t MAX_ELEMS
void drawVLine(int16_t x, int16_t y, int16_t h)
const uint8_t * buffer() const
Packed pixel rows, strideBytes() * height() bytes.
size_t byteSize() const
Total packed byte size.
bool ok() const
True when the buffer allocation succeeded.
void markDirty() override
void onEnter(void *context) override
const char * customFooter_
static ViewStack & instance()
Returns singleton view-stack instance.
void forceRefresh(hal::RefreshMode mode)
Escalates the refresh mode of the next render.
static constexpr uint8_t MAX_TWEENS
void setArena(uint8_t *buf, uint32_t cap)
Inject the pixel arena. Must happen before create(); resets the store.
static constexpr uint8_t MAX_SPRITES
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.
IKeypad * getKeypadInstance()
Returns the singleton keypad service instance.
@ SPRITE_FLAG_ROT_90
Rotate 90 deg clockwise (before flips).
@ SPRITE_FLAG_OPAQUE
Unset data bits paint white.
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 drawBitmapMasked(Adafruit_GFX *gfx, int16_t x, int16_t y, const uint8_t *data, int16_t w, int16_t h, const BlitOpts &opts, uint16_t fg, uint16_t bg)
Blit a packed 1-bpp bitmap with optional mask plane, flipping, 90-degree rotation,...
void fillTriangleDither(Adafruit_GFX *gfx, int16_t x0, int16_t y0, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint8_t shade, uint16_t color)
Dithered filled triangle; see fillRectDither.
void fillCircleDither(Adafruit_GFX *gfx, int16_t cx, int16_t cy, int16_t r, uint8_t shade, uint16_t color)
Dithered filled circle; see fillRectDither.
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 fillRectDither(Adafruit_GFX *gfx, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t shade, uint16_t color)
Fills a rectangle with an ordered-dither fake-grey pattern.
Centralized key-code constants for cdc_views.
const GFXfont * getGfxFont(FontId id)
Resolves a FontId to its underlying GFX font pointer.
static constexpr char KEY_DOWN
Move selection down (numeric '8').
static constexpr char KEY_NO
Cancel / Back / Backspace.
static constexpr int TITLE_Y
Layout constants mirror the ones used by T9InputView.
static constexpr char KEY_UP
Move selection up (numeric '2').
static constexpr char KEY_YES
Confirm / OK / Save.
Replay accessors: geometry, flags and pixel pointers of a sprite.
Tween start configuration (mirrors host_anim_t plus the blink shorthand).
uint8_t scale
Integer upscale 1..4.
const uint8_t * mask
Mask plane (same layout) or null.
uint16_t srcX
Horizontal source window start.
bool opaque
Without a mask: unset bits paint bg.