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