[FL-3925, FL-3942, FL-3944] JS features & bugfixes (SDK 0.2) (#4075)

* feat: JS GPIO PWM, JS GUI Widget view; fix: JS EvtLoop stop on request, JS EvtLoop stop on error
* fix: f18 build
* docs: widget
* fix: js unit test
* change feature naming

Co-authored-by: あく <alleteam@gmail.com>
This commit is contained in:
Anna Antonenko
2025-02-13 12:50:38 +04:00
committed by GitHub
parent ac1b723436
commit e27f82f041
33 changed files with 858 additions and 104 deletions

View File

@@ -0,0 +1,61 @@
#include "../../js_modules.h"
#include <assets_icons.h>
typedef struct {
const char* name;
const Icon* data;
} IconDefinition;
#define ICON_DEF(icon) \
(IconDefinition) { \
.name = #icon, .data = &I_##icon \
}
static const IconDefinition builtin_icons[] = {
ICON_DEF(DolphinWait_59x54),
ICON_DEF(js_script_10px),
};
static void js_gui_icon_get_builtin(struct mjs* mjs) {
const char* icon_name;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&icon_name));
for(size_t i = 0; i < COUNT_OF(builtin_icons); i++) {
if(strcmp(icon_name, builtin_icons[i].name) == 0) {
mjs_return(mjs, mjs_mk_foreign(mjs, (void*)builtin_icons[i].data));
return;
}
}
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "no such built-in icon");
}
static void* js_gui_icon_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
*object = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, *object) {
JS_FIELD("getBuiltin", MJS_MK_FN(js_gui_icon_get_builtin));
}
return NULL;
}
static void js_gui_icon_destroy(void* inst) {
UNUSED(inst);
}
static const JsModuleDescriptor js_gui_icon_desc = {
"gui__icon",
js_gui_icon_create,
js_gui_icon_destroy,
NULL,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_gui_icon_desc,
};
const FlipperAppPluginDescriptor* js_gui_icon_ep(void) {
return &plugin_descriptor;
}

View File

@@ -247,6 +247,22 @@ static bool
return false;
}
/**
* @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);
for(size_t i = 0; i < mjs_array_length(mjs, children); i++) {
mjs_val_t child = mjs_array_get(mjs, children, i);
if(!data->descriptor->add_child(mjs, data->specific_view, data->custom_data, child))
return false;
}
return true;
}
/**
* @brief `View.set`
*/
@@ -260,6 +276,46 @@ static void js_gui_view_set(struct mjs* mjs) {
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief `View.addChild`
*/
static void js_gui_view_add_child(struct mjs* mjs) {
JsGuiViewData* data = JS_GET_CONTEXT(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");
mjs_val_t child;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ANY(&child));
bool success = data->descriptor->add_child(mjs, data->specific_view, data->custom_data, child);
UNUSED(success);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief `View.resetChildren`
*/
static void js_gui_view_reset_children(struct mjs* mjs) {
JsGuiViewData* data = JS_GET_CONTEXT(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");
data->descriptor->reset_children(data->specific_view, data->custom_data);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief `View.setChildren`
*/
static void js_gui_view_set_children(struct mjs* mjs) {
JsGuiViewData* data = JS_GET_CONTEXT(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");
mjs_val_t children;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ARR(&children));
js_gui_view_internal_set_children(mjs, children, data);
}
/**
* @brief `View` destructor
*/
@@ -283,7 +339,12 @@ static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descr
// generic view API
mjs_val_t view_obj = mjs_mk_object(mjs);
mjs_set(mjs, view_obj, "set", ~0, MJS_MK_FN(js_gui_view_set));
JS_ASSIGN_MULTI(mjs, view_obj) {
JS_FIELD("set", MJS_MK_FN(js_gui_view_set));
JS_FIELD("addChild", MJS_MK_FN(js_gui_view_add_child));
JS_FIELD("resetChildren", MJS_MK_FN(js_gui_view_reset_children));
JS_FIELD("setChildren", MJS_MK_FN(js_gui_view_set_children));
}
// object data
JsGuiViewData* data = malloc(sizeof(JsGuiViewData));
@@ -314,7 +375,7 @@ static void js_gui_vf_make(struct mjs* mjs) {
*/
static void js_gui_vf_make_with(struct mjs* mjs) {
mjs_val_t props;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&props));
JS_FETCH_ARGS_OR_RETURN(mjs, JS_AT_LEAST, JS_ARG_OBJ(&props));
const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs);
// make the object like normal
@@ -334,6 +395,18 @@ static void js_gui_vf_make_with(struct mjs* mjs) {
}
}
// assign children
if(mjs_nargs(mjs) >= 2) {
if(!data->descriptor->add_child || !data->descriptor->reset_children)
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "this View can't have children");
mjs_val_t children = mjs_arg(mjs, 1);
if(!mjs_is_array(children))
JS_ERROR_AND_RETURN(mjs, MJS_BAD_ARGS_ERROR, "argument 1: expected array");
if(!js_gui_view_internal_set_children(mjs, children, data)) return;
}
mjs_return(mjs, view_obj);
}

View File

@@ -50,6 +50,11 @@ typedef void (*JsViewFree)(void* specific_view);
typedef void* (*JsViewCustomMake)(struct mjs* mjs, void* specific_view, mjs_val_t view_obj);
/** @brief Context destruction for glue code */
typedef void (*JsViewCustomDestroy)(void* specific_view, void* custom_state, FuriEventLoop* loop);
/** @brief `addChild` callback for glue code */
typedef bool (
*JsViewAddChild)(struct mjs* mjs, void* specific_view, void* custom_state, mjs_val_t child_obj);
/** @brief `resetChildren` callback for glue code */
typedef void (*JsViewResetChildren)(void* specific_view, void* custom_state);
/**
* @brief Descriptor for a JS view
@@ -66,15 +71,22 @@ typedef struct {
JsViewAlloc alloc;
JsViewGetView get_view;
JsViewFree free;
JsViewCustomMake custom_make; // <! May be NULL
JsViewCustomDestroy custom_destroy; // <! May be NULL
JsViewAddChild add_child; // <! May be NULL
JsViewResetChildren reset_children; // <! May be NULL
size_t prop_cnt; //<! Number of properties visible from JS
JsViewPropDescriptor props[]; // <! Descriptors of properties visible from JS
} JsViewDescriptor;
// Callback ordering:
// alloc -> get_view -> [custom_make (if set)] -> props[i].assign -> [custom_destroy (if_set)] -> free
// \_______________ creation ________________/ \___ usage ___/ \_________ destruction _________/
// +-> add_child -+
// +-> reset_children -+
// alloc -> get_view -> custom_make -+-> props[i].assign -+> custom_destroy -> free
// \__________ creation __________/ \____ use ____/ \___ destruction ____/
/**
* @brief Creates a JS `ViewFactory` object

View File

@@ -0,0 +1,281 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/widget.h>
typedef struct {
FuriMessageQueue* queue;
JsEventLoopContract contract;
} JsWidgetCtx;
#define QUEUE_LEN 2
/**
* @brief Parses position (X and Y) from an element declaration object
*/
static bool element_get_position(struct mjs* mjs, mjs_val_t element, int32_t* x, int32_t* y) {
mjs_val_t x_in = mjs_get(mjs, element, "x", ~0);
mjs_val_t y_in = mjs_get(mjs, element, "y", ~0);
if(!mjs_is_number(x_in) || !mjs_is_number(y_in)) return false;
*x = mjs_get_int32(mjs, x_in);
*y = mjs_get_int32(mjs, y_in);
return true;
}
/**
* @brief Parses size (W and h) from an element declaration object
*/
static bool element_get_size(struct mjs* mjs, mjs_val_t element, int32_t* w, int32_t* h) {
mjs_val_t w_in = mjs_get(mjs, element, "w", ~0);
mjs_val_t h_in = mjs_get(mjs, element, "h", ~0);
if(!mjs_is_number(w_in) || !mjs_is_number(h_in)) return false;
*w = mjs_get_int32(mjs, w_in);
*h = mjs_get_int32(mjs, h_in);
return true;
}
/**
* @brief Parses alignment (V and H) from an element declaration object
*/
static bool
element_get_alignment(struct mjs* mjs, mjs_val_t element, Align* align_v, Align* align_h) {
mjs_val_t align_in = mjs_get(mjs, element, "align", ~0);
const char* align = mjs_get_string(mjs, &align_in, NULL);
if(!align) return false;
if(strlen(align) != 2) return false;
if(align[0] == 't') {
*align_v = AlignTop;
} else if(align[0] == 'c') {
*align_v = AlignCenter;
} else if(align[0] == 'b') {
*align_v = AlignBottom;
} else {
return false;
}
if(align[1] == 'l') {
*align_h = AlignLeft;
} else if(align[1] == 'm') { // m = middle
*align_h = AlignCenter;
} else if(align[1] == 'r') {
*align_h = AlignRight;
} else {
return false;
}
return true;
}
/**
* @brief Parses font from an element declaration object
*/
static bool element_get_font(struct mjs* mjs, mjs_val_t element, Font* font) {
mjs_val_t font_in = mjs_get(mjs, element, "font", ~0);
const char* font_str = mjs_get_string(mjs, &font_in, NULL);
if(!font_str) return false;
if(strcmp(font_str, "primary") == 0) {
*font = FontPrimary;
} else if(strcmp(font_str, "secondary") == 0) {
*font = FontSecondary;
} else if(strcmp(font_str, "keyboard") == 0) {
*font = FontKeyboard;
} else if(strcmp(font_str, "big_numbers") == 0) {
*font = FontBigNumbers;
} else {
return false;
}
return true;
}
/**
* @brief Parses text from an element declaration object
*/
static bool element_get_text(struct mjs* mjs, mjs_val_t element, mjs_val_t* text) {
*text = mjs_get(mjs, element, "text", ~0);
return mjs_is_string(*text);
}
/**
* @brief Widget button element callback
*/
static void js_widget_button_callback(GuiButtonType result, InputType type, JsWidgetCtx* context) {
UNUSED(type);
furi_check(furi_message_queue_put(context->queue, &result, 0) == FuriStatusOk);
}
#define DESTRUCTURE_OR_RETURN(mjs, child_obj, part, ...) \
if(!element_get_##part(mjs, child_obj, __VA_ARGS__)) \
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element " #part);
static bool js_widget_add_child(
struct mjs* mjs,
Widget* widget,
JsWidgetCtx* context,
mjs_val_t child_obj) {
UNUSED(context);
if(!mjs_is_object(child_obj))
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "child must be an object");
mjs_val_t element_type_term = mjs_get(mjs, child_obj, "element", ~0);
const char* element_type = mjs_get_string(mjs, &element_type_term, NULL);
if(!element_type)
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "child object must have `element` property");
if((strcmp(element_type, "string") == 0) || (strcmp(element_type, "string_multiline") == 0)) {
int32_t x, y;
Align align_v, align_h;
Font font;
mjs_val_t text;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
DESTRUCTURE_OR_RETURN(mjs, child_obj, alignment, &align_v, &align_h);
DESTRUCTURE_OR_RETURN(mjs, child_obj, font, &font);
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
if(strcmp(element_type, "string") == 0) {
widget_add_string_element(
widget, x, y, align_h, align_v, font, mjs_get_string(mjs, &text, NULL));
} else {
widget_add_string_multiline_element(
widget, x, y, align_h, align_v, font, mjs_get_string(mjs, &text, NULL));
}
} else if(strcmp(element_type, "text_box") == 0) {
int32_t x, y, w, h;
Align align_v, align_h;
Font font;
mjs_val_t text;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
DESTRUCTURE_OR_RETURN(mjs, child_obj, alignment, &align_v, &align_h);
DESTRUCTURE_OR_RETURN(mjs, child_obj, font, &font);
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
mjs_val_t strip_to_dots_in = mjs_get(mjs, child_obj, "stripToDots", ~0);
if(!mjs_is_boolean(strip_to_dots_in))
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element stripToDots");
bool strip_to_dots = mjs_get_bool(mjs, strip_to_dots_in);
widget_add_text_box_element(
widget, x, y, w, h, align_h, align_v, mjs_get_string(mjs, &text, NULL), strip_to_dots);
} else if(strcmp(element_type, "text_scroll") == 0) {
int32_t x, y, w, h;
mjs_val_t text;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
widget_add_text_scroll_element(widget, x, y, w, h, mjs_get_string(mjs, &text, NULL));
} else if(strcmp(element_type, "button") == 0) {
mjs_val_t btn_in = mjs_get(mjs, child_obj, "button", ~0);
const char* btn_name = mjs_get_string(mjs, &btn_in, NULL);
if(!btn_name)
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element button");
GuiButtonType btn_type;
if(strcmp(btn_name, "left") == 0) {
btn_type = GuiButtonTypeLeft;
} else if(strcmp(btn_name, "center") == 0) {
btn_type = GuiButtonTypeCenter;
} else if(strcmp(btn_name, "right") == 0) {
btn_type = GuiButtonTypeRight;
} else {
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "incorrect button type");
}
mjs_val_t text;
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
widget_add_button_element(
widget,
btn_type,
mjs_get_string(mjs, &text, NULL),
(ButtonCallback)js_widget_button_callback,
context);
} else if(strcmp(element_type, "icon") == 0) {
int32_t x, y;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
mjs_val_t icon_data_in = mjs_get(mjs, child_obj, "iconData", ~0);
if(!mjs_is_foreign(icon_data_in))
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element iconData");
const Icon* icon = mjs_get_ptr(mjs, icon_data_in);
widget_add_icon_element(widget, x, y, icon);
} else if(strcmp(element_type, "frame") == 0) {
int32_t x, y, w, h;
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
mjs_val_t radius_in = mjs_get(mjs, child_obj, "radius", ~0);
if(!mjs_is_number(radius_in))
JS_ERROR_AND_RETURN_VAL(
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element radius");
int32_t radius = mjs_get_int32(mjs, radius_in);
widget_add_frame_element(widget, x, y, w, h, radius);
}
return true;
}
static void js_widget_reset_children(Widget* widget, void* state) {
UNUSED(state);
widget_reset(widget);
}
static mjs_val_t js_widget_button_event_transformer(
struct mjs* mjs,
FuriMessageQueue* queue,
JsWidgetCtx* context) {
UNUSED(context);
GuiButtonType btn_type;
furi_check(furi_message_queue_get(queue, &btn_type, 0) == FuriStatusOk);
const char* btn_name;
if(btn_type == GuiButtonTypeLeft) {
btn_name = "left";
} else if(btn_type == GuiButtonTypeCenter) {
btn_name = "center";
} else if(btn_type == GuiButtonTypeRight) {
btn_name = "right";
} else {
furi_crash();
}
return mjs_mk_string(mjs, btn_name, ~0, false);
}
static void* js_widget_custom_make(struct mjs* mjs, Widget* widget, mjs_val_t view_obj) {
UNUSED(widget);
JsWidgetCtx* context = malloc(sizeof(JsWidgetCtx));
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(GuiButtonType));
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)js_widget_button_event_transformer,
},
};
mjs_set(mjs, view_obj, "button", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void js_widget_custom_destroy(Widget* widget, JsWidgetCtx* context, FuriEventLoop* loop) {
UNUSED(widget);
furi_event_loop_maybe_unsubscribe(loop, context->queue);
furi_message_queue_free(context->queue);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)widget_alloc,
.free = (JsViewFree)widget_free,
.get_view = (JsViewGetView)widget_get_view,
.custom_make = (JsViewCustomMake)js_widget_custom_make,
.custom_destroy = (JsViewCustomDestroy)js_widget_custom_destroy,
.add_child = (JsViewAddChild)js_widget_add_child,
.reset_children = (JsViewResetChildren)js_widget_reset_children,
.prop_cnt = 0,
.props = {},
};
JS_GUI_VIEW_DEF(widget, &view_descriptor);