CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
PluginUiState.cpp
Go to the documentation of this file.
3#include "cdc_ui/ViewStack.h"
4#include "host_str_conv.h"
5
6#include <cstring>
7#include <string>
8
9extern "C" void* plg_get_active_plugin(void);
10
11namespace cdc::plugin_manager {
12
13namespace {
14
15cdc::ui::ConfirmView::Icon toConfirmIcon(uint8_t icon)
16{
17 switch (icon) {
21 }
22}
23
24// Single recursive mutex shared by every plugin list view. Only one plugin
25// list is the current top view at a time, and all edits target that list, so
26// one process-lifetime lock suffices. It serialises the plg_tick-task buffer
27// swaps against the UI-task render/key reads (see ListView::setEditMutex).
28SemaphoreHandle_t listEditMutex()
29{
30 static SemaphoreHandle_t m = xSemaphoreCreateRecursiveMutex();
31 return m;
32}
33
34// Same idea for the plugin canvas: host calls on the plg_tick task mutate the
35// display list and arenas while the UI task replays them in render(). One
36// process-lifetime lock, since only one plugin canvas exists at a time.
37SemaphoreHandle_t canvasEditMutex()
38{
39 static SemaphoreHandle_t m = xSemaphoreCreateRecursiveMutex();
40 return m;
41}
42
43} // namespace
44
45PluginUiState& PluginUiState::instance() noexcept
46{
47 static PluginUiState s;
48 return s;
49}
50
51// --- View-callback trampolines ---------------------------------------------
52
53void PluginUiState::onListSelect(uint16_t index, void* userData)
54{
55 auto* state = static_cast<ListState*>(userData);
56 if (!state) {
57 auto& s = instance();
58 state = s.list_.get();
59 }
60 if (!state) return;
61 uint32_t item_id = (index < state->count && state->item_ids)
62 ? state->item_ids[index] : 0;
63 uint32_t action = state->select_action_id;
64 PluginManager::instance().dispatchAction(action, index, item_id);
65}
66
67int PluginUiState::setViewEmpty(const char* text)
68{
69 if (!list_ || !list_->view) return HOST_ERR_NOT_FOUND;
70 if (!text || *text == '\0') {
71 list_->view->setEmptyText(nullptr);
72 list_->empty_buf.reset();
73 return HOST_OK;
74 }
75 std::string cp = toDisplay(text);
76 auto buf = psramAlloc<char>(cp.size() + 1);
77 if (!buf) return HOST_ERR_NO_MEMORY;
78 std::memcpy(buf.get(), cp.c_str(), cp.size() + 1);
79 list_->view->setEmptyText(buf.get());
80 list_->empty_buf = std::move(buf);
81 return HOST_OK;
82}
83
84int PluginUiState::setViewFooter(const char* hint)
85{
87 if (!top) return HOST_ERR_NOT_FOUND;
88
89 PsramUniquePtr<char>* slot = nullptr;
90 if (list_ && list_->view && top == list_->view.get()) slot = &list_->footer_buf;
91 else if (confirm_.view && top == confirm_.view.get()) slot = &confirm_.footer_buf;
92 else if (input_.t9_view && top == input_.t9_view.get()) slot = &input_.footer_buf;
93 else if (input_.pin_view && top == input_.pin_view.get()) slot = &input_.footer_buf;
94 else if (input_.slider_view && top == input_.slider_view.get()) slot = &input_.footer_buf;
95 else if (input_.date_view && top == input_.date_view.get()) slot = &input_.footer_buf;
96 else if (input_.time_view && top == input_.time_view.get()) slot = &input_.footer_buf;
97 else if (input_.color_view && top == input_.color_view.get()) slot = &input_.footer_buf;
98 else if (canvas_.view && top == canvas_.view.get()) slot = &canvas_.footer_buf;
99 if (!slot) return HOST_ERR_NOT_FOUND;
100
101 if (!hint || *hint == '\0') {
102 top->setFooterHint(nullptr);
103 slot->reset();
104 return HOST_OK;
105 }
106 std::string cp = toDisplay(hint);
107 auto buf = psramAlloc<char>(cp.size() + 1);
108 if (!buf) return HOST_ERR_NO_MEMORY;
109 std::memcpy(buf.get(), cp.c_str(), cp.size() + 1);
110 top->setFooterHint(buf.get());
111 *slot = std::move(buf);
112 return HOST_OK;
113}
114
115int PluginUiState::setViewLifecycle(uint32_t hide_action_id, uint32_t show_action_id)
116{
117 // setLifecycleHooks is a virtual IView no-op overridden by ViewBase, so the
118 // top view stores the hooks itself - no need to enumerate plugin view types.
120 if (!top) return HOST_ERR_NOT_FOUND;
121
122 lifecycle_hide_action_ = hide_action_id;
123 lifecycle_show_action_ = show_action_id;
124 top->setLifecycleHooks(hide_action_id ? &onViewHide : nullptr,
125 show_action_id ? &onViewShow : nullptr, nullptr);
126 return HOST_OK;
127}
128
130{
131 // Retract this plugin's own modal overlays (context menu / confirm) before
132 // the backing view objects are reset below. showModal() stores a raw pointer
133 // to these members, so one left on the stack would dangle and crash on the
134 // next render once it is freed here. removeModal targets only these specific
135 // views from anywhere in the stack, leaving system modals untouched.
136 auto& vs = cdc::ui::ViewStack::instance();
137 vs.removeModal(ctxmenu_.view.get());
138 vs.removeModal(confirm_.view.get());
139
140 list_.reset();
141 list_graveyard_.clear();
142 ctxmenu_ = ContextMenuState{};
143 confirm_ = ConfirmState{};
144 input_ = InputState{};
145 canvas_ = CanvasState{};
146 exclusive_token_ = nullptr;
147 inactivity_action_ = 0;
148}
149
150void PluginUiState::onListMenu(uint16_t index, void* userData)
151{
152 auto* state = static_cast<ListState*>(userData);
153 if (!state) {
154 auto& s = instance();
155 state = s.list_.get();
156 }
157 if (!state || state->menu_action_id == 0) return;
158 uint32_t item_id = (index < state->count && state->item_ids)
159 ? state->item_ids[index] : 0;
160 PluginManager::instance().dispatchAction(state->menu_action_id, index, item_id);
161}
162
163void PluginUiState::onConfirmYes(void*)
164{
165 auto& s = instance();
166 uint32_t action = s.confirm_.action_id;
167 s.confirm_.action_id = 0;
169}
170
171void PluginUiState::onConfirmNo(void*)
172{
173 auto& s = instance();
174 uint32_t action = s.confirm_.action_id;
175 s.confirm_.action_id = 0;
177}
178
179void PluginUiState::onT9Save(const char* text)
180{
181 auto& s = instance();
182 s.input_.last_text = text ? text : "";
183 uint32_t action = s.input_.action_id;
184 auto len = static_cast<uint32_t>(s.input_.last_text.size());
185 s.input_.action_id = 0;
186 PluginManager::instance().dispatchAction(action, len, 1);
187}
188
189bool PluginUiState::onPinVerify(const char* pin)
190{
191 auto& s = instance();
192 s.input_.last_text = pin ? pin : "";
193 uint32_t action = s.input_.action_id;
194 auto len = static_cast<uint32_t>(s.input_.last_text.size());
195 s.input_.action_id = 0;
196 PluginManager::instance().dispatchAction(action, len, 1);
197 return true;
198}
199
200void PluginUiState::onSliderSave(uint16_t value)
201{
202 auto& s = instance();
203 s.input_.last_int = static_cast<int32_t>(value);
204 s.input_.has_int = true;
205 uint32_t action = s.input_.action_id;
206 s.input_.action_id = 0;
207 PluginManager::instance().dispatchAction(action, value, 1);
208}
209
210void PluginUiState::onDateSave(uint8_t day, uint8_t month, uint16_t year)
211{
212 auto& s = instance();
213 s.input_.last_date = (static_cast<uint32_t>(year) << 16) |
214 (static_cast<uint32_t>(month) << 8) |
215 static_cast<uint32_t>(day);
216 uint32_t action = s.input_.action_id;
217 s.input_.action_id = 0;
218 PluginManager::instance().dispatchAction(action, s.input_.last_date, 1);
219}
220
221void PluginUiState::onTimeSave(uint8_t hour, uint8_t minute)
222{
223 auto& s = instance();
224 s.input_.last_time = static_cast<uint16_t>((hour << 8) | minute);
225 uint32_t action = s.input_.action_id;
226 s.input_.action_id = 0;
227 PluginManager::instance().dispatchAction(action, s.input_.last_time, 1);
228}
229
230void PluginUiState::onColorSave(uint8_t r, uint8_t g, uint8_t b)
231{
232 auto& s = instance();
233 uint32_t packed = (static_cast<uint32_t>(r) << 16)
234 | (static_cast<uint32_t>(g) << 8)
235 | static_cast<uint32_t>(b);
236 s.input_.last_int = static_cast<int32_t>(packed);
237 s.input_.has_int = true;
238 uint32_t action = s.input_.action_id;
239 s.input_.action_id = 0;
240 PluginManager::instance().dispatchAction(action, packed, 1);
241}
242
243void PluginUiState::onInputCancel()
244{
245 // Cancel path for the self-popping input views (T9, slider, date, time,
246 // color): the view already popped itself, so just report the dismissal with
247 // idx = 0 and user_data = 0 (the confirm path uses user_data = 1).
248 auto& s = instance();
249 uint32_t action = s.input_.action_id;
250 s.input_.action_id = 0;
251 if (action) PluginManager::instance().dispatchAction(action, 0, 0);
252}
253
254void PluginUiState::onPinCancel()
255{
256 // PinEntryView leaves dismissal to its cancel callback, so pop it here
257 // before reporting, mirroring host_ui_pop semantics for the other inputs.
259 auto& s = instance();
260 uint32_t action = s.input_.action_id;
261 s.input_.action_id = 0;
262 if (action) PluginManager::instance().dispatchAction(action, 0, 0);
263}
264
265void PluginUiState::onInactivity()
266{
267 uint32_t action = instance().inactivity_action_;
268 if (action) PluginManager::instance().dispatchAction(action, 0, 0);
269}
270
271void PluginUiState::onViewHide(void* /*userData*/)
272{
273 uint32_t action = instance().lifecycle_hide_action_;
274 if (action) PluginManager::instance().dispatchAction(action, 0, 0);
275}
276
277void PluginUiState::onViewShow(void* /*userData*/)
278{
279 uint32_t action = instance().lifecycle_show_action_;
280 if (action) PluginManager::instance().dispatchAction(action, 0, 0);
281}
282
283void PluginUiState::onCanvasKey(char key, uint32_t focused_widget)
284{
285 uint32_t action = instance().canvas_.key_action_id;
286 if (action) {
288 action, focused_widget, static_cast<uint32_t>(static_cast<unsigned char>(key)));
289 }
290}
291
292void PluginUiState::onCanvasLongPress(char key)
293{
294 uint32_t action = instance().canvas_.long_press_action_id;
295 if (action) {
297 action, 0, static_cast<uint32_t>(static_cast<unsigned char>(key)));
298 }
299}
300
301void PluginUiState::onCanvasWidget(uint32_t widget_id, cdc::ui::CanvasView::WidgetEvent event)
302{
303 uint32_t action = instance().canvas_.widget_action_id;
304 if (action) {
306 action, widget_id, static_cast<uint32_t>(event));
307 }
308}
309
310void PluginUiState::onCanvasAnim(uint32_t action_id, uint32_t handle, uint32_t ref_id)
311{
312 // The action id was supplied per tween/sprite playback (done_action_id),
313 // so it goes out verbatim: plugin_on_action(action_id, handle, ref_id).
314 if (action_id) {
315 PluginManager::instance().dispatchAction(action_id, handle, ref_id);
316 }
317}
318
319// --- View push API ----------------------------------------------------------
320
321namespace {
322
331
332constexpr void (*kCtxCallbacks[cdc::ui::ContextMenuView::MAX_ITEMS])() = {
333 &ctxCb0, &ctxCb1, &ctxCb2, &ctxCb3,
334 &ctxCb4, &ctxCb5, &ctxCb6, &ctxCb7,
335};
336
337} // namespace
338
340{
341 if (idx >= ctxmenu_.count) return;
342 uint32_t item_id = ctxmenu_.item_ids ? ctxmenu_.item_ids[idx] : 0;
343 uint32_t action = ctxmenu_.select_action_id;
344 PluginManager::instance().dispatchAction(action, idx, item_id);
345}
346
347int PluginUiState::pushContextMenu(const char* title, const ui_item_t* items, uint16_t count,
348 uint32_t select_action_id)
349{
350 if (count == 0 || !items) return HOST_ERR_INVALID_ARG;
352
353 ContextMenuState next;
354 std::string cpLabels[cdc::ui::ContextMenuView::MAX_ITEMS];
355 size_t pool_bytes = 0;
356 for (uint16_t i = 0; i < count; ++i) {
357 cpLabels[i] = toDisplay(items[i].label);
358 pool_bytes += cpLabels[i].size() + 1;
359 }
360 next.items = psramAlloc<cdc::ui::ContextMenuItem>(count);
361 next.string_pool = psramAlloc<char>(pool_bytes + 1);
362 next.item_ids = psramAlloc<uint32_t>(count);
363 if (!next.items || !next.string_pool || !next.item_ids) {
364 return HOST_ERR_NO_MEMORY;
365 }
366
367 char* dst = next.string_pool.get();
368 for (uint16_t i = 0; i < count; ++i) {
369 size_t n = cpLabels[i].size();
370 std::memcpy(dst, cpLabels[i].c_str(), n);
371 dst[n] = '\0';
372 next.items[i].label = dst;
373 next.items[i].callback = kCtxCallbacks[i];
374 next.item_ids[i] = items[i].item_id;
375 dst += n + 1;
376 }
377 next.count = count;
378 next.select_action_id = select_action_id;
379 if (title && *title) {
380 std::string cpTitle = toDisplay(title);
381 next.title_buf = psramAlloc<char>(cpTitle.size() + 1);
382 if (!next.title_buf) return HOST_ERR_NO_MEMORY;
383 std::memcpy(next.title_buf.get(), cpTitle.c_str(), cpTitle.size() + 1);
384 }
385 next.view = std::make_unique<cdc::ui::ContextMenuView>();
386 next.view->init(next.title_buf ? next.title_buf.get() : "",
387 next.items.get(), static_cast<uint8_t>(count));
388
389 cdc::ui::ViewStack::instance().showModal(next.view.get());
390 ctxmenu_ = std::move(next);
391 return HOST_OK;
392}
393
394int PluginUiState::pushList(const char* title, const ui_item_t* items, uint16_t count,
395 uint32_t select_action_id, uint32_t menu_action_id,
396 bool replace_top)
397{
398 if (!title || (!items && count > 0)) return HOST_ERR_INVALID_ARG;
399
400 auto next = std::make_unique<ListState>();
401 if (count > 0) {
402 next->items = psramAlloc<cdc::ui::ListItem>(count);
403 next->item_ids = psramAlloc<uint32_t>(count);
404 if (!next->items || !next->item_ids) {
405 return HOST_ERR_NO_MEMORY;
406 }
407 next->labels.reserve(count);
408 }
409 next->capacity = count;
410
411 for (uint16_t i = 0; i < count; ++i) {
412 std::string cp = toDisplay(items[i].label);
413 auto buf = psramAlloc<char>(cp.size() + 1);
414 if (!buf) return HOST_ERR_NO_MEMORY;
415 std::memcpy(buf.get(), cp.c_str(), cp.size() + 1);
416 next->items[i].label = buf.get();
417 next->items[i].icon = items[i].icon;
418 next->items[i].iconDisabled = items[i].icon_disabled;
419 next->items[i].userData = next.get();
420 next->item_ids[i] = items[i].item_id;
421 next->labels.push_back(std::move(buf));
422 }
423 next->count = count;
424 next->select_action_id = select_action_id;
425 next->menu_action_id = menu_action_id;
426 std::string cpTitle = toDisplay(title);
427 next->title_buf = psramAlloc<char>(cpTitle.size() + 1);
428 if (!next->title_buf) return HOST_ERR_NO_MEMORY;
429 std::memcpy(next->title_buf.get(), cpTitle.c_str(), cpTitle.size() + 1);
430 next->view = std::make_unique<cdc::ui::ListView>();
431 next->view->setEditMutex(listEditMutex());
432 next->view->init(next->title_buf.get(), next->items.get(), count);
433 next->view->setOnSelect(&PluginUiState::onListSelect);
434 if (menu_action_id != 0) next->view->setOnMenu(&PluginUiState::onListMenu);
435
436 auto& stack = cdc::ui::ViewStack::instance();
437 const bool can_replace = (list_ && list_->view && stack.current() == list_->view.get());
438 if (can_replace && replace_top) {
439 stack.replace(next->view.get());
440 } else {
441 stack.push(next->view.get());
442 }
443 if (list_) {
444 list_graveyard_.push_back(std::move(list_));
445 }
446 list_ = std::move(next);
447 return HOST_OK;
448}
449
450int PluginUiState::updateListItem(uint16_t index, const ui_item_t* item)
451{
452 if (!item) return HOST_ERR_INVALID_ARG;
453 if (!list_ || !list_->view || !list_->items || !list_->item_ids) {
454 return HOST_ERR_NOT_FOUND;
455 }
456 if (index >= list_->count) return HOST_ERR_INVALID_ARG;
457
458 // Only mutate while our list is the active top view.
459 if (cdc::ui::ViewStack::instance().current() != list_->view.get()) {
460 return HOST_ERR_NOT_FOUND;
461 }
462
463 // The packed string_pool labels cannot grow in place, so the new label
464 // lives in a per-item override buffer owned by this ListState.
465 std::string cp = toDisplay(item->label);
466 auto buf = psramAlloc<char>(cp.size() + 1);
467 if (!buf) return HOST_ERR_NO_MEMORY;
468 std::memcpy(buf.get(), cp.c_str(), cp.size() + 1);
469
470 {
471 // Hold the list edit lock so the UI task does not read items_[index]
472 // while its label pointer is being re-pointed at the new buffer.
473 cdc::core::RecursiveMutexGuard guard(listEditMutex());
474 list_->items[index].label = buf.get();
475 list_->items[index].icon = item->icon;
476 list_->items[index].iconDisabled = item->icon_disabled;
477 list_->item_ids[index] = item->item_id;
478 list_->labels[index] = std::move(buf);
479
480 list_->view->updateItem(index);
481 }
482 return HOST_OK;
483}
484
485bool PluginUiState::growList(uint16_t need)
486{
487 if (!list_) return false;
488 if (list_->capacity >= need) return true;
489
490 uint16_t newCap = static_cast<uint16_t>(list_->capacity + list_->capacity / 2 + 8);
491 if (newCap < need) newCap = need;
493
494 auto ni = psramAlloc<cdc::ui::ListItem>(newCap);
495 auto nid = psramAlloc<uint32_t>(newCap);
496 if (!ni || !nid) return false;
497 for (uint16_t i = 0; i < list_->count; ++i) {
498 ni[i] = list_->items[i];
499 nid[i] = list_->item_ids[i];
500 }
501 list_->items = std::move(ni);
502 list_->item_ids = std::move(nid);
503 list_->capacity = newCap;
504 // The backing array moved, so the view must be re-pointed. This is the only
505 // init on the insert path and amortises to O(log count) over many inserts.
506 list_->view->preservePosition();
507 list_->view->init(list_->title_buf ? list_->title_buf.get() : "",
508 list_->items.get(), list_->count);
509 return true;
510}
511
512int PluginUiState::insertListItem(uint16_t index, const ui_item_t* item)
513{
514 if (!item) return HOST_ERR_INVALID_ARG;
515 if (!list_ || !list_->view) {
516 return HOST_ERR_NOT_FOUND;
517 }
518 if (cdc::ui::ViewStack::instance().current() != list_->view.get()) {
519 return HOST_ERR_NOT_FOUND;
520 }
521 const uint16_t oldCount = list_->count;
522 if (oldCount >= cdc::ui::ListView::MAX_ITEMS) return HOST_ERR_NO_MEMORY;
523 if (index > oldCount) index = oldCount;
524
525 std::string cp = toDisplay(item->label);
526 auto buf = psramAlloc<char>(cp.size() + 1);
527 if (!buf) return HOST_ERR_NO_MEMORY;
528 std::memcpy(buf.get(), cp.c_str(), cp.size() + 1);
529
530 {
531 // Serialise with the UI task: it must not read items_ mid-shift.
532 cdc::core::RecursiveMutexGuard guard(listEditMutex());
533 if (!growList(static_cast<uint16_t>(oldCount + 1))) return HOST_ERR_NO_MEMORY;
534 // Shift rows [index, oldCount) up by one within the capacity array.
535 for (uint16_t r = oldCount; r > index; --r) {
536 list_->items[r] = list_->items[r - 1];
537 list_->item_ids[r] = list_->item_ids[r - 1];
538 }
539 list_->labels.insert(list_->labels.begin() + index, std::move(buf));
540 list_->items[index].label = list_->labels[index].get();
541 list_->items[index].icon = item->icon;
542 list_->items[index].iconDisabled = item->icon_disabled;
543 list_->items[index].userData = list_.get();
544 list_->item_ids[index] = item->item_id;
545 list_->count = static_cast<uint16_t>(oldCount + 1);
546 list_->view->insertItem(index);
547 }
548 return HOST_OK;
549}
550
552{
553 if (!list_ || !list_->view || !list_->items || !list_->item_ids) {
554 return HOST_ERR_NOT_FOUND;
555 }
556 if (cdc::ui::ViewStack::instance().current() != list_->view.get()) {
557 return HOST_ERR_NOT_FOUND;
558 }
559 const uint16_t oldCount = list_->count;
560 if (index >= oldCount) return HOST_ERR_INVALID_ARG;
561
562 {
563 // Serialise with the UI task: it must not read items_ mid-shift.
564 cdc::core::RecursiveMutexGuard guard(listEditMutex());
565 list_->labels.erase(list_->labels.begin() + index);
566 // Shift rows (index, oldCount) down by one within the capacity array.
567 for (uint16_t r = index; r + 1 < oldCount; ++r) {
568 list_->items[r] = list_->items[r + 1];
569 list_->item_ids[r] = list_->item_ids[r + 1];
570 }
571 list_->count = static_cast<uint16_t>(oldCount - 1);
572 list_->view->removeItem(index);
573 }
574 return HOST_OK;
575}
576
577int PluginUiState::pushConfirm(const char* text, uint8_t icon, uint32_t action_id)
578{
579 if (!text) return HOST_ERR_INVALID_ARG;
580 confirm_ = ConfirmState{};
581 confirm_.view = std::make_unique<cdc::ui::ConfirmView>();
582 confirm_.action_id = action_id;
583 std::string cpText = toDisplay(text);
584 confirm_.view->init(cpText.c_str(), toConfirmIcon(icon));
585 confirm_.view->setOnConfirm(&PluginUiState::onConfirmYes, nullptr);
586 confirm_.view->setOnCancel (&PluginUiState::onConfirmNo, nullptr);
587 cdc::ui::ViewStack::instance().showModal(confirm_.view.get());
588 return HOST_OK;
589}
590
591int PluginUiState::pushT9(const char* title, const char* initial,
592 uint16_t max_len, uint32_t action_id)
593{
594 if (!title || max_len == 0) return HOST_ERR_INVALID_ARG;
595 input_ = InputState{};
596 input_.action_id = action_id;
597 std::string cpTitle = toDisplay(title);
598 input_.title_buf = psramAlloc<char>(cpTitle.size() + 1);
599 if (!input_.title_buf) return HOST_ERR_NO_MEMORY;
600 std::memcpy(input_.title_buf.get(), cpTitle.c_str(), cpTitle.size() + 1);
601 std::string cpInitial = toDisplay(initial);
602 input_.t9_view = std::make_unique<cdc::ui::T9InputView>();
603 input_.t9_view->init(input_.title_buf.get(), initial ? cpInitial.c_str() : nullptr, max_len);
604 input_.t9_view->setOnSave(&PluginUiState::onT9Save);
605 input_.t9_view->setOnCancel(&PluginUiState::onInputCancel);
606 cdc::ui::ViewStack::instance().push(input_.t9_view.get());
607 return HOST_OK;
608}
609
610int PluginUiState::pushPin(const char* title, uint8_t max_len, uint8_t max_attempts,
611 uint32_t action_id)
612{
613 if (!title || max_len == 0) return HOST_ERR_INVALID_ARG;
614 input_ = InputState{};
615 input_.action_id = action_id;
616 std::string cpTitle = toDisplay(title);
617 input_.pin_view = std::make_unique<cdc::ui::PinEntryView>();
618 input_.pin_view->init(cpTitle.c_str(), max_len, max_attempts);
619 input_.pin_view->setOnVerify(&PluginUiState::onPinVerify);
620 input_.pin_view->setOnCancel(&PluginUiState::onPinCancel);
621 cdc::ui::ViewStack::instance().push(input_.pin_view.get());
622 return HOST_OK;
623}
624
625int PluginUiState::pushSlider(const char* title, int32_t min, int32_t max, int32_t init,
626 int32_t step, const char* unit, uint32_t action_id)
627{
628 if (!title || min >= max) return HOST_ERR_INVALID_ARG;
629 input_ = InputState{};
630 input_.action_id = action_id;
631 std::string cpTitle = toDisplay(title);
632 input_.title_buf = psramAlloc<char>(cpTitle.size() + 1);
633 if (!input_.title_buf) return HOST_ERR_NO_MEMORY;
634 std::memcpy(input_.title_buf.get(), cpTitle.c_str(), cpTitle.size() + 1);
635 std::string cpUnit = toDisplay(unit);
636 input_.unit_buf = psramAlloc<char>(cpUnit.size() + 1);
637 if (!input_.unit_buf) return HOST_ERR_NO_MEMORY;
638 std::memcpy(input_.unit_buf.get(), cpUnit.c_str(), cpUnit.size() + 1);
639 input_.slider_view = std::make_unique<cdc::ui::SliderView>();
640 input_.slider_view->init(input_.title_buf.get(), min, max, init, step, input_.unit_buf.get());
641 input_.slider_view->setOnSave(&PluginUiState::onSliderSave);
642 input_.slider_view->setOnCancel(&PluginUiState::onInputCancel);
643 cdc::ui::ViewStack::instance().push(input_.slider_view.get());
644 return HOST_OK;
645}
646
647int PluginUiState::pushDate(const char* title, uint8_t d, uint8_t m, uint16_t y,
648 uint32_t action_id)
649{
650 if (!title) return HOST_ERR_INVALID_ARG;
651 input_ = InputState{};
652 input_.action_id = action_id;
653 std::string cpTitle = toDisplay(title);
654 input_.date_view = std::make_unique<cdc::ui::DateInputView>();
655 input_.date_view->init(cpTitle.c_str(), d, m, y);
656 input_.date_view->setOnConfirm(&PluginUiState::onDateSave);
657 input_.date_view->setOnCancel(&PluginUiState::onInputCancel);
658 cdc::ui::ViewStack::instance().push(input_.date_view.get());
659 return HOST_OK;
660}
661
662int PluginUiState::pushTime(const char* title, uint8_t h, uint8_t m, uint32_t action_id)
663{
664 if (!title) return HOST_ERR_INVALID_ARG;
665 input_ = InputState{};
666 input_.action_id = action_id;
667 std::string cpTitle = toDisplay(title);
668 input_.time_view = std::make_unique<cdc::ui::TimeInputView>();
669 input_.time_view->init(cpTitle.c_str(), h, m);
670 input_.time_view->setOnConfirm(&PluginUiState::onTimeSave);
671 input_.time_view->setOnCancel(&PluginUiState::onInputCancel);
672 cdc::ui::ViewStack::instance().push(input_.time_view.get());
673 return HOST_OK;
674}
675
676int PluginUiState::pushColorPicker(uint8_t r, uint8_t g, uint8_t b, uint32_t action_id)
677{
678 input_ = InputState{};
679 input_.action_id = action_id;
680 input_.color_view = std::make_unique<cdc::ui::ColorPickerView>();
681 input_.color_view->init(r, g, b);
682 input_.color_view->setOnSave(&PluginUiState::onColorSave);
683 input_.color_view->setOnCancel(&PluginUiState::onInputCancel);
684 cdc::ui::ViewStack::instance().push(input_.color_view.get());
685 return HOST_OK;
686}
687
688int PluginUiState::pushCanvas(const char* title, uint32_t key_action_id,
689 uint32_t widget_action_id)
690{
691 canvas_ = CanvasState{};
692
693 if (title && *title) {
694 std::string cp = toDisplay(title);
695 canvas_.title_buf = psramAlloc<char>(cp.size() + 1);
696 if (!canvas_.title_buf) return HOST_ERR_NO_MEMORY;
697 std::memcpy(canvas_.title_buf.get(), cp.c_str(), cp.size() + 1);
698 }
699
700 canvas_.key_action_id = key_action_id;
701 canvas_.widget_action_id = widget_action_id;
702 canvas_.view = std::make_unique<cdc::ui::CanvasView>();
703 canvas_.view->setEditMutex(canvasEditMutex());
704 canvas_.view->init(canvas_.title_buf.get());
705 canvas_.view->setKeyCallback(&PluginUiState::onCanvasKey);
706 canvas_.view->setWidgetCallback(&PluginUiState::onCanvasWidget);
707 canvas_.view->setAnimCallback(&PluginUiState::onCanvasAnim);
708
709 cdc::ui::ViewStack::instance().push(canvas_.view.get());
710 return HOST_OK;
711}
712
714{
715 if (!canvas_.view) return HOST_ERR_NOT_FOUND;
716 canvas_.long_press_action_id = action_id;
717 canvas_.view->setLongPressCallback(action_id ? &PluginUiState::onCanvasLongPress : nullptr);
718 return HOST_OK;
719}
720
722{
723 return canvas_.view.get();
724}
725
727{
728 void* plugin = plg_get_active_plugin();
729 if (!plugin) return HOST_ERR_NO_CAPABILITY;
730 exclusive_token_ = plugin;
732}
733
735{
736 if (!exclusive_token_) return HOST_ERR_NOT_FOUND;
737 bool ok = cdc::ui::ViewStack::instance().releaseExclusive(exclusive_token_);
738 exclusive_token_ = nullptr;
739 return ok ? HOST_OK : HOST_ERR_GENERIC;
740}
741
742int PluginUiState::setInactivity(uint32_t timeout_ms, uint32_t action_id)
743{
744 inactivity_action_ = action_id;
746 action_id ? &PluginUiState::onInactivity : nullptr,
747 action_id ? timeout_ms : 0);
748 return HOST_OK;
749}
750
751int PluginUiState::consumeInputText(char* out, size_t out_size)
752{
753 if (!out || out_size == 0) return HOST_ERR_INVALID_ARG;
754 int n = copyUtf8(input_.last_text.c_str(), out, out_size);
755 input_.last_text.clear();
756 return n;
757}
758
760{
761 if (!out) return HOST_ERR_INVALID_ARG;
762 if (!input_.has_int) return HOST_ERR_NOT_FOUND; // no int input pending
763 *out = input_.last_int;
764 input_.last_int = 0;
765 input_.has_int = false;
766 return HOST_OK;
767}
768
769} // namespace cdc::plugin_manager
Discovers, loads, runs and unloads WASM plugins on the badge.
void * plg_get_active_plugin(void)
Singleton that owns plugin-pushed UI views (lists, confirms, inputs).
Scoped guard for a FreeRTOS recursive mutex.
Definition Raii.h:245
void dispatchAction(uint32_t action_id, uint32_t idx, uint32_t user_data)
static PluginManager & instance() noexcept
int pushPin(const char *title, uint8_t max_len, uint8_t max_attempts, uint32_t action_id)
int setCanvasLongPressAction(uint32_t action_id)
int pushCanvas(const char *title, uint32_t key_action_id, uint32_t widget_action_id)
int setViewLifecycle(uint32_t hide_action_id, uint32_t show_action_id)
int pushContextMenu(const char *title, const ui_item_t *items, uint16_t count, uint32_t select_action_id)
int removeListItem(uint16_t index)
Remove the row at index from the current plugin list (rebuild + partial refresh).
int insertListItem(uint16_t index, const ui_item_t *item)
Insert a row at index into the current plugin list (rebuild + partial refresh).
int pushList(const char *title, const ui_item_t *items, uint16_t count, uint32_t select_action_id, uint32_t menu_action_id, bool replace_top=false)
int pushTime(const char *title, uint8_t h, uint8_t m, uint32_t action_id)
int setViewFooter(const char *hint)
int pushT9(const char *title, const char *initial, uint16_t max_len, uint32_t action_id)
int pushDate(const char *title, uint8_t d, uint8_t m, uint16_t y, uint32_t action_id)
int pushConfirm(const char *text, uint8_t icon, uint32_t action_id)
int updateListItem(uint16_t index, const ui_item_t *item)
Redraw a single row of the current plugin list in place (partial refresh).
cdc::ui::CanvasView * canvasView()
int consumeInputText(char *out, size_t out_size)
int pushSlider(const char *title, int32_t min, int32_t max, int32_t init, int32_t step, const char *unit, uint32_t action_id)
int setInactivity(uint32_t timeout_ms, uint32_t action_id)
static PluginUiState & instance() noexcept
int pushColorPicker(uint8_t r, uint8_t g, uint8_t b, uint32_t action_id)
Generic canvas view exposed to WASM plugins for custom UIs.
Definition CanvasView.h:26
static constexpr uint8_t MAX_ITEMS
static constexpr uint16_t MAX_ITEMS
Definition ListView.h:36
void setInactivityTimeout(InactivityCallback callback, uint32_t timeoutMs)
IView * current() const
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:53
void showModal(IView *modal)
bool releaseExclusive(const void *owner)
Releases exclusive ownership.
bool acquireExclusive(const void *owner)
Acquires exclusive ownership of the view stack.
void push(IView *view, void *context=nullptr)
#define UI_ICON_ERROR
Definition host_api.h:834
#define UI_ICON_ALERT
Definition host_api.h:851
#define HOST_ERR_NO_CAPABILITY
Definition host_api.h:40
#define HOST_OK
Definition host_api.h:37
#define HOST_ERR_INVALID_ARG
Definition host_api.h:39
#define HOST_ERR_NO_MEMORY
Definition host_api.h:43
#define HOST_ERR_NOT_FOUND
Definition host_api.h:41
#define HOST_ERR_GENERIC
Definition host_api.h:38
#define HOST_ERR_BUSY
Definition host_api.h:44
void * plg_get_active_plugin(void)
Internal UTF-8 <-> CP437 helpers for the plugin host API boundary.
PsramUniquePtr< T > psramAlloc(std::size_t count) noexcept
Definition Raii.h:27
::cdc::core::PsramUniquePtr< T > PsramUniquePtr
Definition Raii.h:21
std::string toDisplay(const char *utf8)
Decode a UTF-8 (with optional HTML entities) string into CP437 bytes.
int copyUtf8(const char *cp437, char *out, size_t out_size)
Encode a CP437 string into a caller buffer as UTF-8.
uint8_t icon
Definition host_api.h:825
uint32_t item_id
Definition host_api.h:827
bool icon_disabled
Definition host_api.h:826
const char * label
Definition host_api.h:824