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
@@ -19,6 +19,7 @@ typedef struct {
bool connected;
bool running;
int rate;
enum HidMouseButtons btn;
} HidMouseClickerModel;
static void hid_mouse_clicker_start_or_restart_timer(void* context) {
@@ -61,6 +62,26 @@ static void hid_mouse_clicker_draw_callback(Canvas* canvas, void* context) {
// Ok
canvas_draw_icon(canvas, 58, 25, &I_Space_65x18);
canvas_draw_icon(canvas, 61, 50, &I_ButtonLeft_4x7);
canvas_draw_icon(canvas, 117, 50, &I_ButtonRight_4x7);
const char* btn_label;
switch(model->btn) {
case HID_MOUSE_BTN_LEFT:
btn_label = "Left";
break;
case HID_MOUSE_BTN_WHEEL:
btn_label = "Middle";
break;
case HID_MOUSE_BTN_RIGHT:
btn_label = "Right";
break;
default:
furi_crash();
}
elements_multiline_text_aligned(canvas, 89, 57, AlignCenter, AlignBottom, btn_label);
if(model->running) {
elements_slightly_rounded_box(canvas, 61, 27, 60, 13);
canvas_set_color(canvas, ColorWhite);
@@ -100,8 +121,8 @@ static void hid_mouse_clicker_timer_callback(void* context) {
HidMouseClickerModel * model,
{
if(model->running) {
hid_hal_mouse_press(hid_mouse_clicker->hid, HID_MOUSE_BTN_LEFT);
hid_hal_mouse_release(hid_mouse_clicker->hid, HID_MOUSE_BTN_LEFT);
hid_hal_mouse_press(hid_mouse_clicker->hid, model->btn);
hid_hal_mouse_release(hid_mouse_clicker->hid, model->btn);
}
},
false);
@@ -154,6 +175,34 @@ static bool hid_mouse_clicker_input_callback(InputEvent* event, void* context) {
case InputKeyBack:
model->running = false;
break;
case InputKeyLeft:
switch(model->btn) {
case HID_MOUSE_BTN_LEFT:
model->btn = HID_MOUSE_BTN_RIGHT;
break;
case HID_MOUSE_BTN_WHEEL:
model->btn = HID_MOUSE_BTN_LEFT;
break;
case HID_MOUSE_BTN_RIGHT:
model->btn = HID_MOUSE_BTN_WHEEL;
break;
}
consumed = true;
break;
case InputKeyRight:
switch(model->btn) {
case HID_MOUSE_BTN_LEFT:
model->btn = HID_MOUSE_BTN_WHEEL;
break;
case HID_MOUSE_BTN_WHEEL:
model->btn = HID_MOUSE_BTN_RIGHT;
break;
case HID_MOUSE_BTN_RIGHT:
model->btn = HID_MOUSE_BTN_LEFT;
break;
}
consumed = true;
break;
default:
consumed = true;
break;
@@ -188,7 +237,10 @@ HidMouseClicker* hid_mouse_clicker_alloc(Hid* hid) {
with_view_model(
hid_mouse_clicker->view,
HidMouseClickerModel * model,
{ model->rate = DEFAULT_CLICK_RATE; },
{
model->rate = DEFAULT_CLICK_RATE;
model->btn = HID_MOUSE_BTN_LEFT;
},
true);
return hid_mouse_clicker;
@@ -91,6 +91,54 @@ App(
sources=["modules/js_gui/text_input.c"],
)
App(
appid="js_gui__number_input",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_number_input_ep",
requires=["js_app"],
sources=["modules/js_gui/number_input.c"],
)
App(
appid="js_gui__button_panel",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_button_panel_ep",
requires=["js_app"],
sources=["modules/js_gui/button_panel.c"],
)
App(
appid="js_gui__popup",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_popup_ep",
requires=["js_app"],
sources=["modules/js_gui/popup.c"],
)
App(
appid="js_gui__button_menu",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_button_menu_ep",
requires=["js_app"],
sources=["modules/js_gui/button_menu.c"],
)
App(
appid="js_gui__menu",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_menu_ep",
requires=["js_app"],
sources=["modules/js_gui/menu.c"],
)
App(
appid="js_gui__vi_list",
apptype=FlipperAppType.PLUGIN,
entry_point="js_view_vi_list_ep",
requires=["js_app"],
sources=["modules/js_gui/vi_list.c"],
)
App(
appid="js_gui__byte_input",
apptype=FlipperAppType.PLUGIN,
@@ -9,9 +9,16 @@ let byteInputView = require("gui/byte_input");
let textBoxView = require("gui/text_box");
let dialogView = require("gui/dialog");
let filePicker = require("gui/file_picker");
let buttonMenuView = require("gui/button_menu");
let buttonPanelView = require("gui/button_panel");
let menuView = require("gui/menu");
let numberInputView = require("gui/number_input");
let popupView = require("gui/popup");
let viListView = require("gui/vi_list");
let widget = require("gui/widget");
let icon = require("gui/icon");
let flipper = require("flipper");
let math = require("math");
// declare clock widget children
let cuteDolphinWithWatch = icon.getBuiltin("DolphinWait_59x54");
@@ -26,6 +33,11 @@ let stopwatchWidgetElements = [
{ element: "button", button: "right", text: "Back" },
];
// icons for the button panel
let offIcons = [icon.getBuiltin("off_19x20"), icon.getBuiltin("off_hover_19x20")];
let powerIcons = [icon.getBuiltin("power_19x20"), icon.getBuiltin("power_hover_19x20")];
let settingsIcon = icon.getBuiltin("Settings_14");
// declare view instances
let views = {
loading: loadingView.make(),
@@ -47,19 +59,57 @@ let views = {
text: "This is a very long string that demonstrates the TextBox view. Use the D-Pad to scroll backwards and forwards.\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rhoncus est malesuada quam egestas ultrices. Maecenas non eros a nulla eleifend vulputate et ut risus. Quisque in mauris mattis, venenatis risus eget, aliquam diam. Fusce pretium feugiat mauris, ut faucibus ex volutpat in. Phasellus volutpat ex sed gravida consectetur. Aliquam sed lectus feugiat, tristique lectus et, bibendum lacus. Ut sit amet augue eu sapien elementum aliquam quis vitae tortor. Vestibulum quis commodo odio. In elementum fermentum massa, eu pellentesque nibh cursus at. Integer eleifend lacus nec purus elementum sodales. Nulla elementum neque urna, non vulputate massa semper sed. Fusce ut nisi vitae dui blandit congue pretium vitae turpis.",
}),
stopwatchWidget: widget.makeWith({}, stopwatchWidgetElements),
buttonMenu: buttonMenuView.makeWith({
header: "Header"
}, [
{ type: "common", label: "Test" },
{ type: "control", label: "Test2" },
]),
buttonPanel: buttonPanelView.makeWith({
matrixSizeX: 2,
matrixSizeY: 2,
}, [
{ type: "button", x: 0, y: 0, matrixX: 0, matrixY: 0, icon: offIcons[0], iconSelected: offIcons[1] },
{ type: "button", x: 30, y: 30, matrixX: 1, matrixY: 1, icon: powerIcons[0], iconSelected: powerIcons[1] },
{ type: "label", x: 0, y: 50, text: "Label", font: "primary" },
]),
menu: menuView.makeWith({}, [
{ label: "One", icon: settingsIcon },
{ label: "Two", icon: settingsIcon },
{ label: "three", icon: settingsIcon },
]),
numberKbd: numberInputView.makeWith({
header: "Number input",
defaultValue: 100,
minValue: 0,
maxValue: 200,
}),
popup: popupView.makeWith({
header: "Hello",
text: "I'm going to be gone\nin 2 seconds",
}),
viList: viListView.makeWith({}, [
{ label: "One", variants: ["1", "1.0"] },
{ label: "Two", variants: ["2", "2.0"] },
]),
demos: submenuView.makeWith({
header: "Choose a demo",
items: [
"Hourglass screen",
"Empty screen",
"Text input & Dialog",
"Byte input",
"Text box",
"File picker",
"Widget",
"Exit app",
],
}),
}, [
"Hourglass screen",
"Empty screen",
"Text input & Dialog",
"Byte input",
"Text box",
"File picker",
"Widget",
"Button menu",
"Button panel",
"Menu",
"Number input",
"Popup",
"Var. item list",
"Exit app",
]),
};
// Enable illegal filename symbols since we're not choosing filenames, gives more flexibility
@@ -97,6 +147,19 @@ eventLoop.subscribe(views.demos.chosen, function (_sub, index, gui, eventLoop, v
} else if (index === 6) {
gui.viewDispatcher.switchTo(views.stopwatchWidget);
} else if (index === 7) {
gui.viewDispatcher.switchTo(views.buttonMenu);
} else if (index === 8) {
gui.viewDispatcher.switchTo(views.buttonPanel);
} else if (index === 9) {
gui.viewDispatcher.switchTo(views.menu);
} else if (index === 10) {
gui.viewDispatcher.switchTo(views.numberKbd);
} else if (index === 11) {
views.popup.set("timeout", 2000);
gui.viewDispatcher.switchTo(views.popup);
} else if (index === 12) {
gui.viewDispatcher.switchTo(views.viList);
} else if (index === 13) {
eventLoop.stop();
}
}, gui, eventLoop, views);
@@ -144,7 +207,7 @@ eventLoop.subscribe(views.stopwatchWidget.button, function (_sub, buttonEvent, g
// count time
eventLoop.subscribe(eventLoop.timer("periodic", 500), function (_sub, _item, views, stopwatchWidgetElements, halfSeconds) {
let text = (halfSeconds / 2 / 60).toString();
let text = math.floor(halfSeconds / 2 / 60).toString();
if (halfSeconds < 10 * 60 * 2)
text = "0" + text;
@@ -152,7 +215,7 @@ eventLoop.subscribe(eventLoop.timer("periodic", 500), function (_sub, _item, vie
if (((halfSeconds / 2) % 60) < 10)
text += "0";
text += ((halfSeconds / 2) % 60).toString();
text += (math.floor(halfSeconds / 2) % 60).toString();
stopwatchWidgetElements[0].text = text;
views.stopwatchWidget.setChildren(stopwatchWidgetElements);
@@ -161,6 +224,42 @@ eventLoop.subscribe(eventLoop.timer("periodic", 500), function (_sub, _item, vie
return [views, stopwatchWidgetElements, halfSeconds];
}, views, stopwatchWidgetElements, 0);
// go back after popup times out
eventLoop.subscribe(views.popup.timeout, function (_sub, _item, gui, views) {
gui.viewDispatcher.switchTo(views.demos);
}, gui, views);
// button menu callback
eventLoop.subscribe(views.buttonMenu.input, function (_sub, input, gui, views) {
views.helloDialog.set("text", "You selected #" + input.index.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// button panel callback
eventLoop.subscribe(views.buttonPanel.input, function (_sub, input, gui, views) {
views.helloDialog.set("text", "You selected #" + input.index.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// menu callback
eventLoop.subscribe(views.menu.chosen, function (_sub, index, gui, views) {
views.helloDialog.set("text", "You selected #" + index.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// menu callback
eventLoop.subscribe(views.numberKbd.input, function (_sub, number, gui, views) {
views.helloDialog.set("text", "You typed " + number.toString());
views.helloDialog.set("center", "Cool!");
gui.viewDispatcher.switchTo(views.helloDialog);
}, gui, views);
// ignore VI list
eventLoop.subscribe(views.viList.valueUpdate, function (_sub, _item) {});
// run UI
gui.viewDispatcher.switchTo(views.demos);
eventLoop.run();
+1 -5
View File
@@ -27,7 +27,7 @@ typedef struct {
// - an rbtree because i deemed it more tedious to implement, and with the
// amount of modules in use (under 10 in the overwhelming majority of cases)
// i bet it's going to be slower than a plain array
ARRAY_DEF(JsModuleArray, JsModuleData, M_POD_OPLIST);
ARRAY_DEF(JsModuleArray, JsModuleData, M_POD_OPLIST); //-V658
#define M_OPL_JsModuleArray_t() ARRAY_OPLIST(JsModuleArray)
static const JsModuleDescriptor modules_builtin[] = {
@@ -272,10 +272,6 @@ void js_check_sdk_compatibility(struct mjs* mjs) {
static const char* extra_features[] = {
"baseline", // dummy "feature"
"gpio-pwm",
"gui-widget",
"serial-framing",
"gui-widget-extras",
// extra modules
"blebeacon",
+2 -2
View File
@@ -16,8 +16,8 @@ extern "C" {
#define JS_SDK_VENDOR_FIRMWARE "momentum"
#define JS_SDK_VENDOR "flipperdevices"
#define JS_SDK_MAJOR 0
#define JS_SDK_MINOR 3
#define JS_SDK_MAJOR 1
#define JS_SDK_MINOR 0
/**
* @brief Returns the foreign pointer in `obj["_"]`
@@ -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);
@@ -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);
@@ -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);
@@ -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
*
@@ -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)));
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -20,7 +20,7 @@ typedef struct {
char* data;
} PatternArrayItem;
ARRAY_DEF(PatternArray, PatternArrayItem, M_POD_OPLIST);
ARRAY_DEF(PatternArray, PatternArrayItem, M_POD_OPLIST); //-V658
static void
js_serial_on_async_rx(FuriHalSerialHandle* handle, FuriHalSerialRxEvent event, void* context) {
@@ -1,6 +1,6 @@
{
"name": "@next-flip/create-fz-app-mntm",
"version": "0.1.1",
"version": "0.1.2",
"description": "Template package for JS apps for Flipper Zero using Momentum Custom Firmware JS SDK",
"bin": "index.js",
"type": "module",
@@ -6,7 +6,7 @@
"start": "npm run build && node node_modules/@next-flip/fz-sdk-mntm/sdk.js upload"
},
"devDependencies": {
"@next-flip/fz-sdk-mntm": "^0.3",
"@next-flip/fz-sdk-mntm": "^1.0",
"typescript": "^5.6.3"
}
}
+3 -3
View File
@@ -72,7 +72,7 @@
* @brief Checks compatibility between the script and the JS SDK that the
* firmware provides
*
* @note You're looking at JS SDK v0.3
* @note You're looking at JS SDK v1.0
*
* @param expectedMajor JS SDK major version expected by the script
* @param expectedMinor JS SDK minor version expected by the script
@@ -92,7 +92,7 @@ declare function sdkCompatibilityStatus(expectedMajor: number, expectedMinor: nu
* @brief Checks compatibility between the script and the JS SDK that the
* firmware provides in a boolean fashion
*
* @note You're looking at JS SDK v0.3
* @note You're looking at JS SDK v1.0
*
* @param expectedMajor JS SDK major version expected by the script
* @param expectedMinor JS SDK minor version expected by the script
@@ -105,7 +105,7 @@ declare function isSdkCompatible(expectedMajor: number, expectedMinor: number):
* @brief Asks the user whether to continue executing the script if the versions
* are not compatible. Does nothing if they are.
*
* @note You're looking at JS SDK v0.3
* @note You're looking at JS SDK v1.0
*
* @param expectedMajor JS SDK major version expected by the script
* @param expectedMinor JS SDK minor version expected by the script
@@ -80,6 +80,7 @@ export interface Pin {
* PWM-related methods on this pin will throw an error when called.
* @note On Flipper Zero only pins PA4 and PA7 support PWM
* @version Added in JS SDK 0.2, extra feature `"gpio-pwm"`
* @version Baseline since JS SDK 1.0
*/
isPwmSupported(): boolean;
/**
@@ -89,18 +90,21 @@ export interface Pin {
* @param freq Frequency in Hz
* @param duty Duty cycle in %
* @version Added in JS SDK 0.2, extra feature `"gpio-pwm"`
* @version Baseline since JS SDK 1.0
*/
pwmWrite(freq: number, duty: number): void;
/**
* Determines whether PWM is running. Throws an error if PWM is not
* supported on this pin.
* @version Added in JS SDK 0.2, extra feature `"gpio-pwm"`
* @version Baseline since JS SDK 1.0
*/
isPwmRunning(): boolean;
/**
* Stops PWM. Does not restore previous pin configuration. Throws an error
* if PWM is not supported on this pin.
* @version Added in JS SDK 0.2, extra feature `"gpio-pwm"`
* @version Baseline since JS SDK 1.0
*/
pwmStop(): void;
}
@@ -0,0 +1,40 @@
/**
* Displays a list of buttons.
*
* <img src="../images/button_menu.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let buttonMenuView = require("gui/button_menu");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* - `header`: Textual header above the buttons
*
* @version Added in JS SDK 0.4
* @module
*/
import type { View, ViewFactory, InputType } from ".";
import type { Contract } from "../event_loop";
type Props = {
header: string;
};
type Child = { type: "common" | "control", label: string };
declare class ButtonMenu extends View<Props, Child> {
input: Contract<{ index: number, type: InputType }>;
}
declare class ButtonMenuFactory extends ViewFactory<Props, Child, ButtonMenu> { }
declare const factory: ButtonMenuFactory;
export = factory;
@@ -0,0 +1,49 @@
/**
* Displays a button matrix.
*
* <img src="../images/button_panel.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let buttonPanelView = require("gui/button_panel");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* - `matrixSizeX`: Width of imaginary grid used for navigation
* - `matrixSizeY`: Height of imaginary grid used for navigation
*
* @version Added in JS SDK 0.4
* @module
*/
import type { View, ViewFactory, Font, InputType } from ".";
import type { Contract } from "../event_loop";
import { IconData } from "./icon";
type Props = {
matrixSizeX: number,
matrixSizeY: number,
};
type Position = { x: number, y: number };
type ButtonChild = { type: "button", matrixX: number, matrixY: number, icon: IconData, iconSelected: IconData } & Position;
type LabelChild = { type: "label", font: Font, text: string } & Position;
type IconChild = { type: "icon", icon: IconData };
type Child = ButtonChild | LabelChild | IconChild;
declare class ButtonPanel extends View<Props, Child> {
input: Contract<{ index: number, type: InputType }>;
}
declare class ButtonPanelFactory extends ViewFactory<Props, Child, ButtonPanel> { }
declare const factory: ButtonPanelFactory;
export = factory;
@@ -7,6 +7,7 @@ export type IconData = symbol & { "__tag__": "icon" };
* Gets a built-in firmware icon for use in GUI
* @param icon Name of the icon
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
* @version Baseline since JS SDK 1.0
*/
export declare function getBuiltin(icon: BuiltinIcon): IconData;
@@ -14,5 +15,6 @@ export declare function getBuiltin(icon: BuiltinIcon): IconData;
* Loads a .fxbm icon (XBM Flipper sprite, from flipperzero-game-engine) for use in GUI
* @param path Path to the .fxbm file
* @version Added in JS SDK 0.3, extra feature `"gui-widget-extras"`
* @version Baseline since JS SDK 1.0
*/
export declare function loadFxbm(path: string): IconData;
+16 -9
View File
@@ -24,24 +24,23 @@
* ### View
* In Flipper's terminology, a "View" is a fullscreen design element that
* assumes control over the entire viewport and all input events. Different
* types of views are available (not all of which are unfortunately currently
* implemented in JS):
* types of views are available:
* | View | Has JS adapter? |
* |----------------------|-----------------------|
* | `button_menu` | |
* | `button_panel` | |
* | `button_menu` | |
* | `button_panel` | |
* | `byte_input` | ✅ |
* | `dialog_ex` | ✅ (as `dialog`) |
* | `empty_screen` | ✅ |
* | `file_browser` | ✅ (as `file_picker`) |
* | `loading` | ✅ |
* | `menu` | |
* | `number_input` | |
* | `popup` | |
* | `menu` | |
* | `number_input` | |
* | `popup` | |
* | `submenu` | ✅ |
* | `text_box` | ✅ |
* | `text_input` | ✅ |
* | `variable_item_list` | |
* | `variable_item_list` | ✅ (as `vi_list`) |
* | `widget` | ✅ |
*
* In JS, each view has its own set of properties (or just "props"). The
@@ -119,6 +118,9 @@
import type { Contract } from "../event_loop";
export type Font = "primary" | "secondary" | "keyboard" | "big_numbers";
export type InputType = "press" | "release" | "short" | "long" | "repeat";
type Properties = { [K: string]: any };
export declare class View<Props extends Properties, Child> {
@@ -133,17 +135,20 @@ export declare class View<Props extends Properties, Child> {
* Adds a child to the View
* @param child Child to add
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
* @version Baseline since JS SDK 1.0
*/
addChild<C extends Child>(child: C): void;
/**
* Removes all children from the View
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
* @version Baseline since JS SDK 1.0
*/
resetChildren(): void;
/**
* Removes all previous children from the View and assigns new children
* @param children The list of children to assign
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
* @version Baseline since JS SDK 1.0
*/
setChildren(children: Child[]): void;
}
@@ -158,7 +163,9 @@ export declare class ViewFactory<Props extends Properties, Child, V extends View
* Create view instance with custom values, can be changed later with set()
* @param initial Dictionary of property names to values
* @param children Optional list of children to add to the view
* @version Added in JS SDK 0.1; amended in JS SDK 0.2, extra feature `"gui-widget"`
* @version Added in JS SDK 0.1
* @version Amended in JS SDK 0.2, extra feature `"gui-widget"`
* @version Baseline since JS SDK 1.0
*/
makeWith(initial: Partial<Props>, children?: Child[]): V;
}
@@ -0,0 +1,38 @@
/**
* A list of selectable entries consisting of an icon and a label.
*
* <img src="../images/menu.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let submenuView = require("gui/menu");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the GUI example.
*
* # View props
* This view doesn't have any props.
*
* @version Added in JS SDK 0.1
* @version API changed in JS SDK 0.4
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
import type { IconData } from "./icon";
type Props = {};
type Child = { icon: IconData, label: string };
declare class Submenu extends View<Props, Child> {
chosen: Contract<number>;
}
declare class SubmenuFactory extends ViewFactory<Props, Child, Submenu> { }
declare const factory: SubmenuFactory;
export = factory;
@@ -0,0 +1,44 @@
/**
* Displays a number input keyboard.
*
* <img src="../images/number_input.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let numberInputView = require("gui/number_input");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* - `header`: Text displayed at the top of the screen
* - `minValue`: Minimum allowed numeric value
* - `maxValue`: Maximum allowed numeric value
* - `defaultValue`: Default numeric value
*
* @version Added in JS SDK 0.4
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
type Props = {
header: string,
minValue: number,
maxValue: number,
defaultValue: number,
}
type Child = never;
declare class NumberInput extends View<Props, Child> {
input: Contract<number>;
}
declare class NumberInputFactory extends ViewFactory<Props, Child, NumberInput> { }
declare const factory: NumberInputFactory;
export = factory;
@@ -0,0 +1,43 @@
/**
* Like a Dialog, but with a built-in timer.
*
* <img src="../images/popup.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let popupView = require("gui/popup");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* - `header`: Text displayed in bold at the top of the screen
* - `text`: Text displayed in the middle of the string
* - `timeout`: Timeout, in milliseconds, after which the event will fire. The
* timer starts counting down when this property is assigned.
*
* @version Added in JS SDK 0.1
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
type Props = {
header: string,
text: string,
timeout: number,
}
type Child = never;
declare class Popup extends View<Props, Child> {
timeout: Contract;
}
declare class PopupFactory extends ViewFactory<Props, Child, Popup> { }
declare const factory: PopupFactory;
export = factory;
@@ -18,9 +18,9 @@
*
* # View props
* - `header`: Text displayed at the top of the screen in bold
* - `items`: Array of selectable textual items
*
* @version Added in JS SDK 0.1
* @version API changed in JS SDK 0.4
* @module
*/
@@ -29,9 +29,8 @@ import type { Contract } from "../event_loop";
type Props = {
header: string,
items: string[],
};
type Child = never;
type Child = string;
declare class Submenu extends View<Props, Child> {
chosen: Contract<number>;
}
@@ -0,0 +1,38 @@
/**
* Displays a list of settings-like variable items.
*
* <img src="../images/vi_list.png" width="200" alt="Sample screenshot of the view" />
*
* ```js
* let eventLoop = require("event_loop");
* let gui = require("gui");
* let viListView = require("gui/vi_list");
* ```
*
* This module depends on the `gui` module, which in turn depends on the
* `event_loop` module, so they _must_ be imported in this order. It is also
* recommended to conceptualize these modules first before using this one.
*
* # Example
* For an example refer to the `gui.js` example script.
*
* # View props
* This view doesn't have any props
*
* @version Added in JS SDK 0.4
* @module
*/
import type { View, ViewFactory } from ".";
import type { Contract } from "../event_loop";
type Props = {};
type Child = { label: string, variants: string[] };
declare class ViList extends View<Props, Child> {
valueUpdate: Contract<{ itemIndex: number, valueIndex: number }>;
}
declare class ViListFactory extends ViewFactory<Props, Child, ViList> { }
declare const factory: ViListFactory;
export = factory;
+16 -3
View File
@@ -23,6 +23,7 @@
* This view has the elements as its children.
*
* @version Added in JS SDK 0.2, extra feature `"gui-widget"`
* @version Baseline since JS SDK 1.0
* @module
*/
@@ -42,9 +43,21 @@ type TextBoxElement = { element: "text_box", stripToDots: boolean } & Position &
type TextScrollElement = { element: "text_scroll" } & Position & Size & Text;
type ButtonElement = { element: "button", button: "left" | "center" | "right" } & Text;
type IconElement = { element: "icon", iconData: IconData } & Position;
type RectElement = { element: "rect", radius: number, fill: boolean } & Position & Size; /** @version Amended in JS SDK 0.3, extra feature `"gui-widget-extras"` */
type CircleElement = { element: "circle", radius: number, fill: boolean } & Position; /** @version Added in JS SDK 0.3, extra feature `"gui-widget-extras"` */
type LineElement = { element: "line", x1: number, y1: number, x2: number, y2: number }; /** @version Added in JS SDK 0.3, extra feature `"gui-widget-extras"` */
/**
* @version Amended in JS SDK 0.3, extra feature `"gui-widget-extras"`
* @version Baseline since JS SDK 1.0
* */
type RectElement = { element: "rect", radius: number, fill: boolean } & Position & Size;
/**
* @version Added in JS SDK 0.3, extra feature `"gui-widget-extras"`
* @version Baseline since JS SDK 1.0
* */
type CircleElement = { element: "circle", radius: number, fill: boolean } & Position;
/**
* @version Added in JS SDK 0.3, extra feature `"gui-widget-extras"`
* @version Baseline since JS SDK 1.0
* */
type LineElement = { element: "line", x1: number, y1: number, x2: number, y2: number };
type Element = StringMultilineElement
| StringElement
@@ -1,6 +1,6 @@
{
"name": "@next-flip/fz-sdk-mntm",
"version": "0.3.0",
"version": "1.0.0",
"description": "Type declarations and documentation for native JS modules available on Momentum Custom Firmware for Flipper Zero",
"keywords": [
"momentum",
@@ -27,4 +27,4 @@
"prompts": "^2.4.2",
"serialport": "^12.0.0"
}
}
}
@@ -29,6 +29,7 @@ export interface Framing {
* @param framing See `Framing` type
* @version Added in JS SDK 0.1
* @version Added `framing` parameter in JS SDK 0.3, extra feature `"serial-framing"`
* @version Baseline since JS SDK 1.0
*/
export declare function setup(port: "lpuart" | "usart", baudRate: number, framing?: Framing): void;