28SemaphoreHandle_t listEditMutex()
30 static SemaphoreHandle_t m = xSemaphoreCreateRecursiveMutex();
37SemaphoreHandle_t canvasEditMutex()
39 static SemaphoreHandle_t m = xSemaphoreCreateRecursiveMutex();
47 static PluginUiState s;
53void PluginUiState::onListSelect(uint16_t index,
void* userData)
55 auto* state =
static_cast<ListState*
>(userData);
58 state = s.list_.get();
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;
70 if (!text || *text ==
'\0') {
71 list_->view->setEmptyText(
nullptr);
72 list_->empty_buf.reset();
78 std::memcpy(buf.get(), cp.c_str(), cp.size() + 1);
79 list_->view->setEmptyText(buf.get());
80 list_->empty_buf = std::move(buf);
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;
101 if (!hint || *hint ==
'\0') {
102 top->setFooterHint(
nullptr);
109 std::memcpy(buf.get(), cp.c_str(), cp.size() + 1);
110 top->setFooterHint(buf.get());
111 *slot = std::move(buf);
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);
137 vs.removeModal(ctxmenu_.view.get());
138 vs.removeModal(confirm_.view.get());
141 list_graveyard_.clear();
142 ctxmenu_ = ContextMenuState{};
143 confirm_ = ConfirmState{};
144 input_ = InputState{};
145 canvas_ = CanvasState{};
146 exclusive_token_ =
nullptr;
147 inactivity_action_ = 0;
150void PluginUiState::onListMenu(uint16_t index,
void* userData)
152 auto* state =
static_cast<ListState*
>(userData);
155 state = s.list_.get();
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;
163void PluginUiState::onConfirmYes(
void*)
166 uint32_t action = s.confirm_.action_id;
167 s.confirm_.action_id = 0;
171void PluginUiState::onConfirmNo(
void*)
174 uint32_t action = s.confirm_.action_id;
175 s.confirm_.action_id = 0;
179void PluginUiState::onT9Save(
const char* text)
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;
189bool PluginUiState::onPinVerify(
const char* pin)
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;
200void PluginUiState::onSliderSave(uint16_t value)
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;
210void PluginUiState::onDateSave(uint8_t day, uint8_t month, uint16_t year)
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;
221void PluginUiState::onTimeSave(uint8_t hour, uint8_t minute)
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;
230void PluginUiState::onColorSave(uint8_t r, uint8_t g, uint8_t b)
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;
243void PluginUiState::onInputCancel()
249 uint32_t action = s.input_.action_id;
250 s.input_.action_id = 0;
254void PluginUiState::onPinCancel()
260 uint32_t action = s.input_.action_id;
261 s.input_.action_id = 0;
265void PluginUiState::onInactivity()
267 uint32_t action =
instance().inactivity_action_;
271void PluginUiState::onViewHide(
void* )
273 uint32_t action =
instance().lifecycle_hide_action_;
277void PluginUiState::onViewShow(
void* )
279 uint32_t action =
instance().lifecycle_show_action_;
283void PluginUiState::onCanvasKey(
char key, uint32_t focused_widget)
285 uint32_t action =
instance().canvas_.key_action_id;
288 action, focused_widget,
static_cast<uint32_t
>(
static_cast<unsigned char>(key)));
292void PluginUiState::onCanvasLongPress(
char key)
294 uint32_t action =
instance().canvas_.long_press_action_id;
297 action, 0,
static_cast<uint32_t
>(
static_cast<unsigned char>(key)));
303 uint32_t action =
instance().canvas_.widget_action_id;
306 action, widget_id,
static_cast<uint32_t
>(event));
310void PluginUiState::onCanvasAnim(uint32_t action_id, uint32_t handle, uint32_t ref_id)
333 &ctxCb0, &ctxCb1, &ctxCb2, &ctxCb3,
334 &ctxCb4, &ctxCb5, &ctxCb6, &ctxCb7,
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;
348 uint32_t select_action_id)
353 ContextMenuState next;
355 size_t pool_bytes = 0;
356 for (uint16_t i = 0; i < count; ++i) {
358 pool_bytes += cpLabels[i].size() + 1;
363 if (!next.items || !next.string_pool || !next.item_ids) {
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);
372 next.items[i].label = dst;
373 next.items[i].callback = kCtxCallbacks[i];
374 next.item_ids[i] = items[i].
item_id;
378 next.select_action_id = select_action_id;
379 if (title && *title) {
383 std::memcpy(next.title_buf.get(), cpTitle.c_str(), cpTitle.size() + 1);
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));
390 ctxmenu_ = std::move(next);
395 uint32_t select_action_id, uint32_t menu_action_id,
400 auto next = std::make_unique<ListState>();
404 if (!next->items || !next->item_ids) {
407 next->labels.reserve(count);
409 next->capacity = count;
411 for (uint16_t i = 0; i < count; ++i) {
412 std::string cp =
toDisplay(items[i].label);
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;
419 next->items[i].userData = next.get();
420 next->item_ids[i] = items[i].
item_id;
421 next->labels.push_back(std::move(buf));
424 next->select_action_id = select_action_id;
425 next->menu_action_id = menu_action_id;
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);
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());
441 stack.push(next->view.get());
444 list_graveyard_.push_back(std::move(list_));
446 list_ = std::move(next);
453 if (!list_ || !list_->view || !list_->items || !list_->item_ids) {
468 std::memcpy(buf.get(), cp.c_str(), cp.size() + 1);
474 list_->items[index].label = buf.get();
475 list_->items[index].icon = item->
icon;
477 list_->item_ids[index] = item->
item_id;
478 list_->labels[index] = std::move(buf);
480 list_->view->updateItem(index);
485bool PluginUiState::growList(uint16_t need)
487 if (!list_)
return false;
488 if (list_->capacity >= need)
return true;
490 uint16_t newCap =
static_cast<uint16_t
>(list_->capacity + list_->capacity / 2 + 8);
491 if (newCap < need) newCap = need;
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];
501 list_->items = std::move(ni);
502 list_->item_ids = std::move(nid);
503 list_->capacity = newCap;
506 list_->view->preservePosition();
507 list_->view->init(list_->title_buf ? list_->title_buf.get() :
"",
508 list_->items.get(), list_->count);
515 if (!list_ || !list_->view) {
521 const uint16_t oldCount = list_->count;
523 if (index > oldCount) index = oldCount;
528 std::memcpy(buf.get(), cp.c_str(), cp.size() + 1);
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];
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;
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);
553 if (!list_ || !list_->view || !list_->items || !list_->item_ids) {
559 const uint16_t oldCount = list_->count;
565 list_->labels.erase(list_->labels.begin() + index);
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];
571 list_->count =
static_cast<uint16_t
>(oldCount - 1);
572 list_->view->removeItem(index);
580 confirm_ = ConfirmState{};
581 confirm_.view = std::make_unique<cdc::ui::ConfirmView>();
582 confirm_.action_id = action_id;
584 confirm_.view->init(cpText.c_str(), toConfirmIcon(icon));
585 confirm_.view->setOnConfirm(&PluginUiState::onConfirmYes,
nullptr);
586 confirm_.view->setOnCancel (&PluginUiState::onConfirmNo,
nullptr);
592 uint16_t max_len, uint32_t action_id)
595 input_ = InputState{};
596 input_.action_id = action_id;
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);
614 input_ = InputState{};
615 input_.action_id = action_id;
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);
626 int32_t step,
const char* unit, uint32_t action_id)
629 input_ = InputState{};
630 input_.action_id = action_id;
634 std::memcpy(input_.title_buf.get(), cpTitle.c_str(), cpTitle.size() + 1);
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);
651 input_ = InputState{};
652 input_.action_id = action_id;
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);
665 input_ = InputState{};
666 input_.action_id = action_id;
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);
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);
689 uint32_t widget_action_id)
691 canvas_ = CanvasState{};
693 if (title && *title) {
697 std::memcpy(canvas_.title_buf.get(), cp.c_str(), cp.size() + 1);
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);
716 canvas_.long_press_action_id = action_id;
717 canvas_.view->setLongPressCallback(action_id ? &PluginUiState::onCanvasLongPress :
nullptr);
723 return canvas_.view.get();
730 exclusive_token_ = plugin;
738 exclusive_token_ =
nullptr;
744 inactivity_action_ = action_id;
746 action_id ? &PluginUiState::onInactivity :
nullptr,
747 action_id ? timeout_ms : 0);
754 int n =
copyUtf8(input_.last_text.c_str(), out, out_size);
755 input_.last_text.clear();
763 *out = input_.last_int;
765 input_.has_int =
false;
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.
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)
void dispatchContextSelect(uint8_t idx)
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).
int setViewEmpty(const char *text)
cdc::ui::CanvasView * canvasView()
void resetForPluginStop()
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)
int consumeInputInt(int32_t *out)
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.
static constexpr uint16_t MAX_ITEMS
void setInactivityTimeout(InactivityCallback callback, uint32_t timeoutMs)
static ViewStack & instance()
Returns singleton view-stack instance.
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 HOST_ERR_NO_CAPABILITY
#define HOST_ERR_INVALID_ARG
#define HOST_ERR_NO_MEMORY
#define HOST_ERR_NOT_FOUND
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
::cdc::core::PsramUniquePtr< T > PsramUniquePtr
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.