Merge branch 'feat/nfc-type-4-final' into mntm-dev

This commit is contained in:
WillyJL
2025-10-05 23:24:30 +02:00
242 changed files with 10348 additions and 697 deletions

View File

@@ -0,0 +1,169 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/button_menu.h>
#include <toolbox/str_buffer.h>
typedef struct {
int32_t next_index;
StrBuffer str_buffer;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsBtnMenuContext;
typedef struct {
int32_t index;
InputType input_type;
} JsBtnMenuEvent;
static const char* js_input_type_to_str(InputType type) {
switch(type) {
case InputTypePress:
return "press";
case InputTypeRelease:
return "release";
case InputTypeShort:
return "short";
case InputTypeLong:
return "long";
case InputTypeRepeat:
return "repeat";
default:
furi_crash();
}
}
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsBtnMenuContext* context) {
UNUSED(context);
JsBtnMenuEvent event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
mjs_val_t event_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, event_obj) {
JS_FIELD("index", mjs_mk_number(mjs, event.index));
JS_FIELD("type", mjs_mk_string(mjs, js_input_type_to_str(event.input_type), ~0, false));
}
return event_obj;
}
static void input_callback(void* ctx, int32_t index, InputType type) {
JsBtnMenuContext* context = ctx;
JsBtnMenuEvent event = {
.index = index,
.input_type = type,
};
furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk);
}
static bool matrix_header_assign(
struct mjs* mjs,
ButtonMenu* menu,
JsViewPropValue value,
JsBtnMenuContext* context) {
UNUSED(mjs);
button_menu_set_header(menu, str_buffer_make_owned_clone(&context->str_buffer, value.string));
return true;
}
static bool js_button_menu_add_child(
struct mjs* mjs,
ButtonMenu* menu,
JsBtnMenuContext* context,
mjs_val_t child_obj) {
static const JsValueEnumVariant js_button_menu_item_type_variants[] = {
{"common", ButtonMenuItemTypeCommon},
{"control", ButtonMenuItemTypeControl},
};
static const JsValueDeclaration js_button_menu_item_type =
JS_VALUE_ENUM(ButtonMenuItemType, js_button_menu_item_type_variants);
static const JsValueDeclaration js_button_menu_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueObjectField js_button_menu_child_fields[] = {
{"type", &js_button_menu_item_type},
{"label", &js_button_menu_string},
};
static const JsValueDeclaration js_button_menu_child =
JS_VALUE_OBJECT(js_button_menu_child_fields);
ButtonMenuItemType item_type;
const char* label;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_menu_child),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&item_type,
&label);
if(status != JsValueParseStatusOk) return false;
button_menu_add_item(
menu,
str_buffer_make_owned_clone(&context->str_buffer, label),
context->next_index++,
input_callback,
item_type,
context);
return true;
}
static void js_button_menu_reset_children(ButtonMenu* menu, JsBtnMenuContext* context) {
context->next_index = 0;
button_menu_reset(menu);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsBtnMenuContext* ctx_make(struct mjs* mjs, ButtonMenu* menu, mjs_val_t view_obj) {
UNUSED(menu);
JsBtnMenuContext* context = malloc(sizeof(JsBtnMenuContext));
*context = (JsBtnMenuContext){
.next_index = 0,
.str_buffer = {0},
.input_queue = furi_message_queue_alloc(1, sizeof(JsBtnMenuEvent)),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(ButtonMenu* input, JsBtnMenuContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)button_menu_alloc,
.free = (JsViewFree)button_menu_free,
.get_view = (JsViewGetView)button_menu_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_button_menu_add_child,
.reset_children = (JsViewResetChildren)js_button_menu_reset_children,
.prop_cnt = 1,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)matrix_header_assign},
}};
JS_GUI_VIEW_DEF(button_menu, &view_descriptor);

View File

@@ -0,0 +1,274 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/button_panel.h>
#include <toolbox/str_buffer.h>
typedef struct {
size_t matrix_x, matrix_y;
int32_t next_index;
StrBuffer str_buffer;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsBtnPanelContext;
typedef struct {
int32_t index;
InputType input_type;
} JsBtnPanelEvent;
static const char* js_input_type_to_str(InputType type) {
switch(type) {
case InputTypePress:
return "press";
case InputTypeRelease:
return "release";
case InputTypeShort:
return "short";
case InputTypeLong:
return "long";
case InputTypeRepeat:
return "repeat";
default:
furi_crash();
}
}
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsBtnPanelContext* context) {
UNUSED(context);
JsBtnPanelEvent event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
mjs_val_t event_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, event_obj) {
JS_FIELD("index", mjs_mk_number(mjs, event.index));
JS_FIELD("type", mjs_mk_string(mjs, js_input_type_to_str(event.input_type), ~0, false));
}
return event_obj;
}
static void input_callback(void* ctx, int32_t index, InputType type) {
JsBtnPanelContext* context = ctx;
JsBtnPanelEvent event = {
.index = index,
.input_type = type,
};
furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk);
}
static bool matrix_size_x_assign(
struct mjs* mjs,
ButtonPanel* panel,
JsViewPropValue value,
JsBtnPanelContext* context) {
UNUSED(mjs);
context->matrix_x = value.number;
button_panel_reserve(panel, context->matrix_x, context->matrix_y);
return true;
}
static bool matrix_size_y_assign(
struct mjs* mjs,
ButtonPanel* panel,
JsViewPropValue value,
JsBtnPanelContext* context) {
UNUSED(mjs);
context->matrix_y = value.number;
button_panel_reserve(panel, context->matrix_x, context->matrix_y);
return true;
}
static bool js_button_panel_add_child(
struct mjs* mjs,
ButtonPanel* panel,
JsBtnPanelContext* context,
mjs_val_t child_obj) {
typedef enum {
JsButtonPanelChildTypeButton,
JsButtonPanelChildTypeLabel,
JsButtonPanelChildTypeIcon,
} JsButtonPanelChildType;
static const JsValueEnumVariant js_button_panel_child_type_variants[] = {
{"button", JsButtonPanelChildTypeButton},
{"label", JsButtonPanelChildTypeLabel},
{"icon", JsButtonPanelChildTypeIcon},
};
static const JsValueDeclaration js_button_panel_child_type =
JS_VALUE_ENUM(JsButtonPanelChildType, js_button_panel_child_type_variants);
static const JsValueDeclaration js_button_panel_number = JS_VALUE_SIMPLE(JsValueTypeInt32);
static const JsValueObjectField js_button_panel_common_fields[] = {
{"type", &js_button_panel_child_type},
{"x", &js_button_panel_number},
{"y", &js_button_panel_number},
};
static const JsValueDeclaration js_button_panel_common =
JS_VALUE_OBJECT(js_button_panel_common_fields);
static const JsValueDeclaration js_button_panel_pointer =
JS_VALUE_SIMPLE(JsValueTypeRawPointer);
static const JsValueObjectField js_button_panel_button_fields[] = {
{"matrixX", &js_button_panel_number},
{"matrixY", &js_button_panel_number},
{"icon", &js_button_panel_pointer},
{"iconSelected", &js_button_panel_pointer},
};
static const JsValueDeclaration js_button_panel_button =
JS_VALUE_OBJECT(js_button_panel_button_fields);
static const JsValueDeclaration js_button_panel_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueObjectField js_button_panel_label_fields[] = {
{"text", &js_button_panel_string},
{"font", &js_gui_font_declaration},
};
static const JsValueDeclaration js_button_panel_label =
JS_VALUE_OBJECT(js_button_panel_label_fields);
static const JsValueObjectField js_button_panel_icon_fields[] = {
{"icon", &js_button_panel_pointer},
};
static const JsValueDeclaration js_button_panel_icon =
JS_VALUE_OBJECT(js_button_panel_icon_fields);
JsButtonPanelChildType child_type;
int32_t x, y;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_common),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&child_type,
&x,
&y);
if(status != JsValueParseStatusOk) return false;
switch(child_type) {
case JsButtonPanelChildTypeButton: {
int32_t matrix_x, matrix_y;
const Icon *icon, *icon_selected;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_button),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&matrix_x,
&matrix_y,
&icon,
&icon_selected);
if(status != JsValueParseStatusOk) return false;
button_panel_add_item(
panel,
context->next_index++,
matrix_x,
matrix_y,
x,
y,
icon,
icon_selected,
(ButtonItemCallback)input_callback,
context);
break;
}
case JsButtonPanelChildTypeLabel: {
const char* text;
Font font;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_label),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&text,
&font);
if(status != JsValueParseStatusOk) return false;
button_panel_add_label(
panel, x, y, font, str_buffer_make_owned_clone(&context->str_buffer, text));
break;
}
case JsButtonPanelChildTypeIcon: {
const Icon* icon;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_button_panel_icon),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&icon);
if(status != JsValueParseStatusOk) return false;
button_panel_add_icon(panel, x, y, icon);
break;
}
}
return true;
}
static void js_button_panel_reset_children(ButtonPanel* panel, JsBtnPanelContext* context) {
context->next_index = 0;
button_panel_reset(panel);
button_panel_reserve(panel, context->matrix_x, context->matrix_y);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsBtnPanelContext* ctx_make(struct mjs* mjs, ButtonPanel* panel, mjs_val_t view_obj) {
UNUSED(panel);
JsBtnPanelContext* context = malloc(sizeof(JsBtnPanelContext));
*context = (JsBtnPanelContext){
.matrix_x = 1,
.matrix_y = 1,
.next_index = 0,
.str_buffer = {0},
.input_queue = furi_message_queue_alloc(1, sizeof(JsBtnPanelEvent)),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(ButtonPanel* input, JsBtnPanelContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)button_panel_alloc,
.free = (JsViewFree)button_panel_free,
.get_view = (JsViewGetView)button_panel_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_button_panel_add_child,
.reset_children = (JsViewResetChildren)js_button_panel_reset_children,
.prop_cnt = 2,
.props = {
(JsViewPropDescriptor){
.name = "matrixSizeX",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)matrix_size_x_assign},
(JsViewPropDescriptor){
.name = "matrixSizeY",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)matrix_size_y_assign},
}};
JS_GUI_VIEW_DEF(button_panel, &view_descriptor);

View File

@@ -31,6 +31,14 @@ typedef struct {
void* custom_data;
} JsGuiViewData;
static const JsValueEnumVariant js_gui_font_variants[] = {
{"primary", FontPrimary},
{"secondary", FontSecondary},
{"keyboard", FontKeyboard},
{"bit_numbers", FontBigNumbers},
};
const JsValueDeclaration js_gui_font_declaration = JS_VALUE_ENUM(Font, js_gui_font_variants);
/**
* @brief Transformer for custom events
*/
@@ -273,9 +281,12 @@ static bool
/**
* @brief Sets the list of children. Not available from JS.
*/
static bool
js_gui_view_internal_set_children(struct mjs* mjs, mjs_val_t children, JsGuiViewData* data) {
data->descriptor->reset_children(data->specific_view, data->custom_data);
static bool js_gui_view_internal_set_children(
struct mjs* mjs,
mjs_val_t children,
JsGuiViewData* data,
bool do_reset) {
if(do_reset) data->descriptor->reset_children(data->specific_view, data->custom_data);
for(size_t i = 0; i < mjs_array_length(mjs, children); i++) {
mjs_val_t child = mjs_array_get(mjs, children, i);
@@ -357,7 +368,7 @@ static void js_gui_view_set_children(struct mjs* mjs) {
if(!data->descriptor->add_child || !data->descriptor->reset_children)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
js_gui_view_internal_set_children(mjs, children, data);
js_gui_view_internal_set_children(mjs, children, data, true);
}
/**
@@ -450,7 +461,7 @@ static void js_gui_vf_make_with(struct mjs* mjs) {
if(!data->descriptor->add_child || !data->descriptor->reset_children)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
if(!js_gui_view_internal_set_children(mjs, children, data)) return;
if(!js_gui_view_internal_set_children(mjs, children, data, false)) return;
}
mjs_return(mjs, view_obj);

View File

@@ -20,6 +20,11 @@ typedef union {
mjs_val_t term;
} JsViewPropValue;
/**
* JS-to-C font enum mapping
*/
extern const JsValueDeclaration js_gui_font_declaration;
/**
* @brief Assigns a value to a view property
*

View File

@@ -1,4 +1,5 @@
#include "js_gui.h"
static constexpr auto js_gui_api_table = sort(create_array_t<sym_entry>(
API_METHOD(js_gui_make_view_factory, mjs_val_t, (struct mjs*, const JsViewDescriptor*))));
API_METHOD(js_gui_make_view_factory, mjs_val_t, (struct mjs*, const JsViewDescriptor*)),
API_VARIABLE(js_gui_font_declaration, const JsValueDeclaration)));

View File

@@ -0,0 +1,105 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/menu.h>
#include <toolbox/str_buffer.h>
typedef struct {
int32_t next_index;
StrBuffer str_buffer;
FuriMessageQueue* queue;
JsEventLoopContract contract;
} JsMenuCtx;
static mjs_val_t choose_transformer(struct mjs* mjs, FuriMessageQueue* queue, void* context) {
UNUSED(context);
uint32_t index;
furi_check(furi_message_queue_get(queue, &index, 0) == FuriStatusOk);
return mjs_mk_number(mjs, (double)index);
}
static void choose_callback(void* context, uint32_t index) {
JsMenuCtx* ctx = context;
furi_check(furi_message_queue_put(ctx->queue, &index, 0) == FuriStatusOk);
}
static bool
js_menu_add_child(struct mjs* mjs, Menu* menu, JsMenuCtx* context, mjs_val_t child_obj) {
static const JsValueDeclaration js_menu_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueDeclaration js_menu_pointer = JS_VALUE_SIMPLE(JsValueTypeRawPointer);
static const JsValueObjectField js_menu_child_fields[] = {
{"icon", &js_menu_pointer},
{"label", &js_menu_string},
};
static const JsValueDeclaration js_menu_child = JS_VALUE_OBJECT(js_menu_child_fields);
const Icon* icon;
const char* label;
JsValueParseStatus status;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_menu_child),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&icon,
&label);
if(status != JsValueParseStatusOk) return false;
menu_add_item(
menu,
str_buffer_make_owned_clone(&context->str_buffer, label),
icon,
context->next_index++,
choose_callback,
context);
return true;
}
static void js_menu_reset_children(Menu* menu, JsMenuCtx* context) {
context->next_index = 0;
menu_reset(menu);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsMenuCtx* ctx_make(struct mjs* mjs, Menu* input, mjs_val_t view_obj) {
UNUSED(input);
JsMenuCtx* context = malloc(sizeof(JsMenuCtx));
context->queue = furi_message_queue_alloc(1, sizeof(uint32_t));
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)choose_transformer,
},
};
mjs_set(mjs, view_obj, "chosen", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(Menu* input, JsMenuCtx* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->queue);
furi_message_queue_free(context->queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)menu_alloc,
.free = (JsViewFree)menu_free,
.get_view = (JsViewGetView)menu_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_menu_add_child,
.reset_children = (JsViewResetChildren)js_menu_reset_children,
.prop_cnt = 0,
.props = {},
};
JS_GUI_VIEW_DEF(menu, &view_descriptor);

View File

@@ -0,0 +1,130 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/number_input.h>
typedef struct {
int32_t default_val, min_val, max_val;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsNumKbdContext;
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsNumKbdContext* context) {
UNUSED(context);
int32_t number;
furi_check(furi_message_queue_get(queue, &number, 0) == FuriStatusOk);
return mjs_mk_number(mjs, number);
}
static void input_callback(void* ctx, int32_t value) {
JsNumKbdContext* context = ctx;
furi_check(furi_message_queue_put(context->input_queue, &value, 0) == FuriStatusOk);
}
static bool header_assign(
struct mjs* mjs,
NumberInput* input,
JsViewPropValue value,
JsNumKbdContext* context) {
UNUSED(mjs);
UNUSED(context);
number_input_set_header_text(input, value.string);
return true;
}
static bool min_val_assign(
struct mjs* mjs,
NumberInput* input,
JsViewPropValue value,
JsNumKbdContext* context) {
UNUSED(mjs);
context->min_val = value.number;
number_input_set_result_callback(
input, input_callback, context, context->default_val, context->min_val, context->max_val);
return true;
}
static bool max_val_assign(
struct mjs* mjs,
NumberInput* input,
JsViewPropValue value,
JsNumKbdContext* context) {
UNUSED(mjs);
context->max_val = value.number;
number_input_set_result_callback(
input, input_callback, context, context->default_val, context->min_val, context->max_val);
return true;
}
static bool default_val_assign(
struct mjs* mjs,
NumberInput* input,
JsViewPropValue value,
JsNumKbdContext* context) {
UNUSED(mjs);
context->default_val = value.number;
number_input_set_result_callback(
input, input_callback, context, context->default_val, context->min_val, context->max_val);
return true;
}
static JsNumKbdContext* ctx_make(struct mjs* mjs, NumberInput* input, mjs_val_t view_obj) {
JsNumKbdContext* context = malloc(sizeof(JsNumKbdContext));
*context = (JsNumKbdContext){
.default_val = 0,
.max_val = 100,
.min_val = 0,
.input_queue = furi_message_queue_alloc(1, sizeof(int32_t)),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
number_input_set_result_callback(
input, input_callback, context, context->default_val, context->min_val, context->max_val);
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(NumberInput* input, JsNumKbdContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)number_input_alloc,
.free = (JsViewFree)number_input_free,
.get_view = (JsViewGetView)number_input_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 4,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)header_assign},
(JsViewPropDescriptor){
.name = "minValue",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)min_val_assign},
(JsViewPropDescriptor){
.name = "maxValue",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)max_val_assign},
(JsViewPropDescriptor){
.name = "defaultValue",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)default_val_assign},
}};
JS_GUI_VIEW_DEF(number_input, &view_descriptor);

View File

@@ -0,0 +1,102 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/popup.h>
#include <toolbox/str_buffer.h>
typedef struct {
StrBuffer str_buffer;
FuriSemaphore* semaphore;
JsEventLoopContract contract;
} JsPopupCtx;
static void timeout_callback(JsPopupCtx* context) {
furi_check(furi_semaphore_release(context->semaphore) == FuriStatusOk);
}
static bool
header_assign(struct mjs* mjs, Popup* popup, JsViewPropValue value, JsPopupCtx* context) {
UNUSED(mjs);
UNUSED(context);
popup_set_header(
popup,
str_buffer_make_owned_clone(&context->str_buffer, value.string),
64,
0,
AlignCenter,
AlignTop);
return true;
}
static bool
text_assign(struct mjs* mjs, Popup* popup, JsViewPropValue value, JsPopupCtx* context) {
UNUSED(mjs);
UNUSED(context);
popup_set_text(
popup,
str_buffer_make_owned_clone(&context->str_buffer, value.string),
64,
32,
AlignCenter,
AlignCenter);
return true;
}
static bool
timeout_assign(struct mjs* mjs, Popup* popup, JsViewPropValue value, JsPopupCtx* context) {
UNUSED(mjs);
UNUSED(context);
popup_set_timeout(popup, value.number);
popup_enable_timeout(popup);
return true;
}
static JsPopupCtx* ctx_make(struct mjs* mjs, Popup* popup, mjs_val_t view_obj) {
JsPopupCtx* context = malloc(sizeof(JsPopupCtx));
context->semaphore = furi_semaphore_alloc(1, 0);
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeSemaphore,
.object = context->semaphore,
.non_timer =
{
.event = FuriEventLoopEventIn,
},
};
mjs_set(mjs, view_obj, "timeout", ~0, mjs_mk_foreign(mjs, &context->contract));
popup_set_callback(popup, (PopupCallback)timeout_callback);
popup_set_context(popup, context);
return context;
}
static void ctx_destroy(Popup* popup, JsPopupCtx* context, FuriEventLoop* loop) {
UNUSED(popup);
furi_event_loop_maybe_unsubscribe(loop, context->semaphore);
furi_semaphore_free(context->semaphore);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)popup_alloc,
.free = (JsViewFree)popup_free,
.get_view = (JsViewGetView)popup_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 3,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)header_assign},
(JsViewPropDescriptor){
.name = "text",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)text_assign},
(JsViewPropDescriptor){
.name = "timeout",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)timeout_assign},
}};
JS_GUI_VIEW_DEF(popup, &view_descriptor);

View File

@@ -6,6 +6,7 @@
#define QUEUE_LEN 2
typedef struct {
int32_t next_index;
FuriMessageQueue* queue;
JsEventLoopContract contract;
} JsSubmenuCtx;
@@ -30,18 +31,24 @@ static bool
return true;
}
static bool items_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) {
UNUSED(mjs);
submenu_reset(submenu);
size_t len = mjs_array_length(mjs, value.term);
for(size_t i = 0; i < len; i++) {
mjs_val_t item = mjs_array_get(mjs, value.term, i);
if(!mjs_is_string(item)) return false;
submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context);
}
static bool js_submenu_add_child(
struct mjs* mjs,
Submenu* submenu,
JsSubmenuCtx* context,
mjs_val_t child_obj) {
const char* str = mjs_get_string(mjs, &child_obj, NULL);
if(!str) return false;
submenu_add_item(submenu, str, context->next_index++, choose_callback, context);
return true;
}
static void js_submenu_reset_children(Submenu* submenu, JsSubmenuCtx* context) {
context->next_index = 0;
submenu_reset(submenu);
}
static JsSubmenuCtx* ctx_make(struct mjs* mjs, Submenu* input, mjs_val_t view_obj) {
UNUSED(input);
JsSubmenuCtx* context = malloc(sizeof(JsSubmenuCtx));
@@ -73,15 +80,13 @@ static const JsViewDescriptor view_descriptor = {
.get_view = (JsViewGetView)submenu_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 2,
.add_child = (JsViewAddChild)js_submenu_add_child,
.reset_children = (JsViewResetChildren)js_submenu_reset_children,
.prop_cnt = 1,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)header_assign},
(JsViewPropDescriptor){
.name = "items",
.type = JsViewPropTypeArr,
.assign = (JsViewPropAssign)items_assign},
}};
JS_GUI_VIEW_DEF(submenu, &view_descriptor);

View File

@@ -0,0 +1,163 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/variable_item_list.h>
#include <toolbox/str_buffer.h>
typedef struct {
StrBuffer str_buffer;
// let mjs do the memory management heavy lifting, store children in a js array
struct mjs* mjs;
mjs_val_t children;
VariableItemList* list;
FuriMessageQueue* input_queue;
JsEventLoopContract contract;
} JsViListContext;
typedef struct {
int32_t item_index;
int32_t value_index;
} JsViListEvent;
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsViListContext* context) {
UNUSED(context);
JsViListEvent event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
mjs_val_t event_obj = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, event_obj) {
JS_FIELD("itemIndex", mjs_mk_number(mjs, event.item_index));
JS_FIELD("valueIndex", mjs_mk_number(mjs, event.value_index));
}
return event_obj;
}
static void js_vi_list_change_callback(VariableItem* item) {
JsViListContext* context = variable_item_get_context(item);
struct mjs* mjs = context->mjs;
uint8_t item_index = variable_item_list_get_selected_item_index(context->list);
uint8_t value_index = variable_item_get_current_value_index(item);
// type safety ensured in add_child
mjs_val_t variants = mjs_array_get(mjs, context->children, item_index);
mjs_val_t variant = mjs_array_get(mjs, variants, value_index);
variable_item_set_current_value_text(item, mjs_get_string(mjs, &variant, NULL));
JsViListEvent event = {
.item_index = item_index,
.value_index = value_index,
};
furi_check(furi_message_queue_put(context->input_queue, &event, 0) == FuriStatusOk);
}
static bool js_vi_list_add_child(
struct mjs* mjs,
VariableItemList* list,
JsViListContext* context,
mjs_val_t child_obj) {
static const JsValueDeclaration js_vi_list_string = JS_VALUE_SIMPLE(JsValueTypeString);
static const JsValueDeclaration js_vi_list_arr = JS_VALUE_SIMPLE(JsValueTypeAnyArray);
static const JsValueDeclaration js_vi_list_int_default_0 =
JS_VALUE_SIMPLE_W_DEFAULT(JsValueTypeInt32, int32_val, 0);
static const JsValueObjectField js_vi_list_child_fields[] = {
{"label", &js_vi_list_string},
{"variants", &js_vi_list_arr},
{"defaultSelected", &js_vi_list_int_default_0},
};
static const JsValueDeclaration js_vi_list_child =
JS_VALUE_OBJECT_W_DEFAULTS(js_vi_list_child_fields);
JsValueParseStatus status;
const char* label;
mjs_val_t variants;
int32_t default_selected;
JS_VALUE_PARSE(
mjs,
JS_VALUE_PARSE_SOURCE_VALUE(&js_vi_list_child),
JsValueParseFlagReturnOnError,
&status,
&child_obj,
&label,
&variants,
&default_selected);
if(status != JsValueParseStatusOk) return false;
size_t variants_cnt = mjs_array_length(mjs, variants);
for(size_t i = 0; i < variants_cnt; i++)
if(!mjs_is_string(mjs_array_get(mjs, variants, i))) return false;
VariableItem* item = variable_item_list_add(
list,
str_buffer_make_owned_clone(&context->str_buffer, label),
variants_cnt,
js_vi_list_change_callback,
context);
variable_item_set_current_value_index(item, default_selected);
mjs_val_t default_variant = mjs_array_get(mjs, variants, default_selected);
variable_item_set_current_value_text(item, mjs_get_string(mjs, &default_variant, NULL));
mjs_array_push(context->mjs, context->children, variants);
return true;
}
static void js_vi_list_reset_children(VariableItemList* list, JsViListContext* context) {
mjs_disown(context->mjs, &context->children);
context->children = mjs_mk_array(context->mjs);
mjs_own(context->mjs, &context->children);
variable_item_list_reset(list);
str_buffer_clear_all_clones(&context->str_buffer);
}
static JsViListContext* ctx_make(struct mjs* mjs, VariableItemList* list, mjs_val_t view_obj) {
JsViListContext* context = malloc(sizeof(JsViListContext));
*context = (JsViListContext){
.str_buffer = {0},
.mjs = mjs,
.children = mjs_mk_array(mjs),
.list = list,
.input_queue = furi_message_queue_alloc(1, sizeof(JsViListEvent)),
};
mjs_own(context->mjs, &context->children);
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->input_queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
mjs_set(mjs, view_obj, "valueUpdate", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(VariableItemList* input, JsViListContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_queue);
furi_message_queue_free(context->input_queue);
str_buffer_clear_all_clones(&context->str_buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)variable_item_list_alloc,
.free = (JsViewFree)variable_item_list_free,
.get_view = (JsViewGetView)variable_item_list_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.add_child = (JsViewAddChild)js_vi_list_add_child,
.reset_children = (JsViewResetChildren)js_vi_list_reset_children,
.prop_cnt = 0,
.props = {},
};
JS_GUI_VIEW_DEF(vi_list, &view_descriptor);