[FL-3893] JS modules (#3841)

* feat: backport js_gpio from unleashed
* feat: backport js_keyboard, TextInputModel::minimum_length from unleashed
* fix: api version inconsistency
* style: js_gpio
* build: fix submodule ._ .
* refactor: js_gpio
* docs: type declarations for gpio
* feat: gpio interrupts
* fix: js_gpio freeing, resetting and minor stylistic changes
* style: js_gpio
* style: mlib array, fixme's
* feat: js_gpio adc
* feat: js_event_loop
* docs: js_event_loop
* feat: js_event_loop subscription cancellation
* feat: js_event_loop + js_gpio integration
* fix: js_event_loop memory leak
* feat: stop event loop on back button
* test: js: basic, math, event_loop
* feat: js_event_loop queue
* feat: js linkage to previously loaded plugins
* build: fix ci errors
* feat: js module ordered teardown
* feat: js_gui_defer_free
* feat: basic hourglass view
* style: JS ASS (Argument Schema for Scripts)
* fix: js_event_loop mem leaks and lifetime problems
* fix: crashing test and pvs false positives
* feat: mjs custom obj destructors, gui submenu view
* refactor: yank js_gui_defer_free (yuck)
* refactor: maybe_unsubscribe
* empty_screen, docs, typing fix-ups
* docs: navigation event & demo
* feat: submenu setHeader
* feat: text_input
* feat: text_box
* docs: text_box availability
* ci: silence irrelevant pvs low priority warning
* style: use furistring
* style: _get_at -> _safe_get
* fix: built-in module name assignment
* feat: js_dialog; refactor, optimize: js_gui
* docs: js_gui
* ci: silence pvs warning: Memory allocation is infallible
* style: fix storage spelling
* feat: foreign pointer signature checks
* feat: js_storage
* docs: js_storage
* fix: my unit test was breaking other tests ;_;
* ci: fix ci?
* Make doxygen happy
* docs: flipper, math, notification, global
* style: review suggestions
* style: review fixups
* fix: badusb demo script
* docs: badusb
* ci: add nofl
* ci: make linter happy
* Bump api version

Co-authored-by: Aleksandr Kutuzov <alleteam@gmail.com>
This commit is contained in:
porta
2024-10-14 21:42:11 +03:00
committed by GitHub
parent 57c438d91a
commit 8a95cb8d6b
114 changed files with 4978 additions and 931 deletions

View File

@@ -0,0 +1,129 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/dialog_ex.h>
#define QUEUE_LEN 2
typedef struct {
FuriMessageQueue* queue;
JsEventLoopContract contract;
} JsDialogCtx;
static mjs_val_t
input_transformer(struct mjs* mjs, FuriMessageQueue* queue, JsDialogCtx* context) {
UNUSED(context);
DialogExResult result;
furi_check(furi_message_queue_get(queue, &result, 0) == FuriStatusOk);
const char* string;
if(result == DialogExResultLeft) {
string = "left";
} else if(result == DialogExResultCenter) {
string = "center";
} else if(result == DialogExResultRight) {
string = "right";
} else {
furi_crash();
}
return mjs_mk_string(mjs, string, ~0, false);
}
static void input_callback(DialogExResult result, JsDialogCtx* context) {
furi_check(furi_message_queue_put(context->queue, &result, 0) == FuriStatusOk);
}
static bool
header_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_header(dialog, value.string, 64, 0, AlignCenter, AlignTop);
return true;
}
static bool
text_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_text(dialog, value.string, 64, 32, AlignCenter, AlignCenter);
return true;
}
static bool
left_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_left_button_text(dialog, value.string);
return true;
}
static bool
center_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_center_button_text(dialog, value.string);
return true;
}
static bool
right_assign(struct mjs* mjs, DialogEx* dialog, JsViewPropValue value, JsDialogCtx* context) {
UNUSED(mjs);
UNUSED(context);
dialog_ex_set_right_button_text(dialog, value.string);
return true;
}
static JsDialogCtx* ctx_make(struct mjs* mjs, DialogEx* dialog, mjs_val_t view_obj) {
JsDialogCtx* context = malloc(sizeof(JsDialogCtx));
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(DialogExResult));
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeQueue,
.object = context->queue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
},
};
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
dialog_ex_set_result_callback(dialog, (DialogExResultCallback)input_callback);
dialog_ex_set_context(dialog, context);
return context;
}
static void ctx_destroy(DialogEx* input, JsDialogCtx* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->queue);
furi_message_queue_free(context->queue);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)dialog_ex_alloc,
.free = (JsViewFree)dialog_ex_free,
.get_view = (JsViewGetView)dialog_ex_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 5,
.props = {
(JsViewPropDescriptor){
.name = "header",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)header_assign},
(JsViewPropDescriptor){
.name = "text",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)text_assign},
(JsViewPropDescriptor){
.name = "left",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)left_assign},
(JsViewPropDescriptor){
.name = "center",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)center_assign},
(JsViewPropDescriptor){
.name = "right",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)right_assign},
}};
JS_GUI_VIEW_DEF(dialog, &view_descriptor);

View File

@@ -0,0 +1,12 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include <gui/modules/empty_screen.h>
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)empty_screen_alloc,
.free = (JsViewFree)empty_screen_free,
.get_view = (JsViewGetView)empty_screen_get_view,
.prop_cnt = 0,
.props = {},
};
JS_GUI_VIEW_DEF(empty_screen, &view_descriptor);

View File

@@ -0,0 +1,348 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "./js_gui.h"
#include <furi.h>
#include <mlib/m-array.h>
#include <gui/view_dispatcher.h>
#include "../js_event_loop/js_event_loop.h"
#include <m-array.h>
#define EVENT_QUEUE_SIZE 16
typedef struct {
uint32_t next_view_id;
FuriEventLoop* loop;
Gui* gui;
ViewDispatcher* dispatcher;
// event stuff
JsEventLoopContract custom_contract;
FuriMessageQueue* custom;
JsEventLoopContract navigation_contract;
FuriSemaphore*
navigation; // FIXME: (-nofl) convert into callback once FuriEventLoop starts supporting this
} JsGui;
// Useful for factories
static JsGui* js_gui;
typedef struct {
uint32_t id;
const JsViewDescriptor* descriptor;
void* specific_view;
void* custom_data;
} JsGuiViewData;
/**
* @brief Transformer for custom events
*/
static mjs_val_t
js_gui_vd_custom_transformer(struct mjs* mjs, FuriEventLoopObject* object, void* context) {
UNUSED(context);
furi_check(object);
FuriMessageQueue* queue = object;
uint32_t event;
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
return mjs_mk_number(mjs, (double)event);
}
/**
* @brief ViewDispatcher custom event callback
*/
static bool js_gui_vd_custom_callback(void* context, uint32_t event) {
furi_check(context);
JsGui* module = context;
furi_check(furi_message_queue_put(module->custom, &event, 0) == FuriStatusOk);
return true;
}
/**
* @brief ViewDispatcher navigation event callback
*/
static bool js_gui_vd_nav_callback(void* context) {
furi_check(context);
JsGui* module = context;
furi_semaphore_release(module->navigation);
return true;
}
/**
* @brief `viewDispatcher.sendCustom`
*/
static void js_gui_vd_send_custom(struct mjs* mjs) {
int32_t event;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_INT32(&event));
JsGui* module = JS_GET_CONTEXT(mjs);
view_dispatcher_send_custom_event(module->dispatcher, (uint32_t)event);
}
/**
* @brief `viewDispatcher.sendTo`
*/
static void js_gui_vd_send_to(struct mjs* mjs) {
enum {
SendDirToFront,
SendDirToBack,
} send_direction;
JS_ENUM_MAP(send_direction, {"front", SendDirToFront}, {"back", SendDirToBack});
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_ENUM(send_direction, "SendDirection"));
JsGui* module = JS_GET_CONTEXT(mjs);
if(send_direction == SendDirToBack) {
view_dispatcher_send_to_back(module->dispatcher);
} else {
view_dispatcher_send_to_front(module->dispatcher);
}
}
/**
* @brief `viewDispatcher.switchTo`
*/
static void js_gui_vd_switch_to(struct mjs* mjs) {
mjs_val_t view;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_OBJ(&view));
JsGuiViewData* view_data = JS_GET_INST(mjs, view);
JsGui* module = JS_GET_CONTEXT(mjs);
view_dispatcher_switch_to_view(module->dispatcher, (uint32_t)view_data->id);
}
static void* js_gui_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
// get event loop
JsEventLoop* js_loop = js_module_get(modules, "event_loop");
if(M_UNLIKELY(!js_loop)) return NULL;
FuriEventLoop* loop = js_event_loop_get_loop(js_loop);
// create C object
JsGui* module = malloc(sizeof(JsGui));
module->loop = loop;
module->gui = furi_record_open(RECORD_GUI);
module->dispatcher = view_dispatcher_alloc_ex(loop);
module->custom = furi_message_queue_alloc(EVENT_QUEUE_SIZE, sizeof(uint32_t));
module->navigation = furi_semaphore_alloc(EVENT_QUEUE_SIZE, 0);
view_dispatcher_attach_to_gui(module->dispatcher, module->gui, ViewDispatcherTypeFullscreen);
view_dispatcher_send_to_front(module->dispatcher);
// subscribe to events and create contracts
view_dispatcher_set_event_callback_context(module->dispatcher, module);
view_dispatcher_set_custom_event_callback(module->dispatcher, js_gui_vd_custom_callback);
view_dispatcher_set_navigation_event_callback(module->dispatcher, js_gui_vd_nav_callback);
module->custom_contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object = module->custom,
.object_type = JsEventLoopObjectTypeQueue,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = js_gui_vd_custom_transformer,
},
};
module->navigation_contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object = module->navigation,
.object_type = JsEventLoopObjectTypeSemaphore,
.non_timer =
{
.event = FuriEventLoopEventIn,
},
};
// create viewDispatcher object
mjs_val_t view_dispatcher = mjs_mk_object(mjs);
JS_ASSIGN_MULTI(mjs, view_dispatcher) {
JS_FIELD(INST_PROP_NAME, mjs_mk_foreign(mjs, module));
JS_FIELD("sendCustom", MJS_MK_FN(js_gui_vd_send_custom));
JS_FIELD("sendTo", MJS_MK_FN(js_gui_vd_send_to));
JS_FIELD("switchTo", MJS_MK_FN(js_gui_vd_switch_to));
JS_FIELD("custom", mjs_mk_foreign(mjs, &module->custom_contract));
JS_FIELD("navigation", mjs_mk_foreign(mjs, &module->navigation_contract));
}
// create API object
mjs_val_t api = mjs_mk_object(mjs);
mjs_set(mjs, api, "viewDispatcher", ~0, view_dispatcher);
*object = api;
js_gui = module;
return module;
}
static void js_gui_destroy(void* inst) {
furi_assert(inst);
JsGui* module = inst;
view_dispatcher_free(module->dispatcher);
furi_event_loop_maybe_unsubscribe(module->loop, module->custom);
furi_event_loop_maybe_unsubscribe(module->loop, module->navigation);
furi_message_queue_free(module->custom);
furi_semaphore_free(module->navigation);
furi_record_close(RECORD_GUI);
free(module);
js_gui = NULL;
}
/**
* @brief Assigns a `View` property. Not available from JS.
*/
static bool
js_gui_view_assign(struct mjs* mjs, const char* name, mjs_val_t value, JsGuiViewData* data) {
const JsViewDescriptor* descriptor = data->descriptor;
for(size_t i = 0; i < descriptor->prop_cnt; i++) {
JsViewPropDescriptor prop = descriptor->props[i];
if(strcmp(prop.name, name) != 0) continue;
// convert JS value to C
JsViewPropValue c_value;
const char* expected_type = NULL;
switch(prop.type) {
case JsViewPropTypeNumber: {
if(!mjs_is_number(value)) {
expected_type = "number";
break;
}
c_value = (JsViewPropValue){.number = mjs_get_int32(mjs, value)};
} break;
case JsViewPropTypeString: {
if(!mjs_is_string(value)) {
expected_type = "string";
break;
}
c_value = (JsViewPropValue){.string = mjs_get_string(mjs, &value, NULL)};
} break;
case JsViewPropTypeArr: {
if(!mjs_is_array(value)) {
expected_type = "array";
break;
}
c_value = (JsViewPropValue){.array = value};
} break;
}
if(expected_type) {
mjs_prepend_errorf(
mjs, MJS_BAD_ARGS_ERROR, "view prop \"%s\" requires %s value", name, expected_type);
return false;
} else {
return prop.assign(mjs, data->specific_view, c_value, data->custom_data);
}
}
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "view has no prop named \"%s\"", name);
return false;
}
/**
* @brief `View.set`
*/
static void js_gui_view_set(struct mjs* mjs) {
const char* name;
mjs_val_t value;
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY, JS_ARG_STR(&name), JS_ARG_ANY(&value));
JsGuiViewData* data = JS_GET_CONTEXT(mjs);
bool success = js_gui_view_assign(mjs, name, value, data);
UNUSED(success);
mjs_return(mjs, MJS_UNDEFINED);
}
/**
* @brief `View` destructor
*/
static void js_gui_view_destructor(struct mjs* mjs, mjs_val_t obj) {
JsGuiViewData* data = JS_GET_INST(mjs, obj);
view_dispatcher_remove_view(js_gui->dispatcher, data->id);
if(data->descriptor->custom_destroy)
data->descriptor->custom_destroy(data->specific_view, data->custom_data, js_gui->loop);
data->descriptor->free(data->specific_view);
free(data);
}
/**
* @brief Creates a `View` object from a descriptor. Not available from JS.
*/
static mjs_val_t js_gui_make_view(struct mjs* mjs, const JsViewDescriptor* descriptor) {
void* specific_view = descriptor->alloc();
View* view = descriptor->get_view(specific_view);
uint32_t view_id = js_gui->next_view_id++;
view_dispatcher_add_view(js_gui->dispatcher, view_id, view);
// 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));
// object data
JsGuiViewData* data = malloc(sizeof(JsGuiViewData));
*data = (JsGuiViewData){
.descriptor = descriptor,
.id = view_id,
.specific_view = specific_view,
.custom_data =
descriptor->custom_make ? descriptor->custom_make(mjs, specific_view, view_obj) : NULL,
};
mjs_set(mjs, view_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, data));
mjs_set(mjs, view_obj, MJS_DESTRUCTOR_PROP_NAME, ~0, MJS_MK_FN(js_gui_view_destructor));
return view_obj;
}
/**
* @brief `ViewFactory.make`
*/
static void js_gui_vf_make(struct mjs* mjs) {
JS_FETCH_ARGS_OR_RETURN(mjs, JS_EXACTLY); // 0 args
const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs);
mjs_return(mjs, js_gui_make_view(mjs, descriptor));
}
/**
* @brief `ViewFactory.makeWith`
*/
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));
const JsViewDescriptor* descriptor = JS_GET_CONTEXT(mjs);
// make the object like normal
mjs_val_t view_obj = js_gui_make_view(mjs, descriptor);
JsGuiViewData* data = JS_GET_INST(mjs, view_obj);
// assign properties one by one
mjs_val_t key, iter = MJS_UNDEFINED;
while((key = mjs_next(mjs, props, &iter)) != MJS_UNDEFINED) {
furi_check(mjs_is_string(key));
const char* name = mjs_get_string(mjs, &key, NULL);
mjs_val_t value = mjs_get(mjs, props, name, ~0);
if(!js_gui_view_assign(mjs, name, value, data)) {
mjs_return(mjs, MJS_UNDEFINED);
return;
}
}
mjs_return(mjs, view_obj);
}
mjs_val_t js_gui_make_view_factory(struct mjs* mjs, const JsViewDescriptor* view_descriptor) {
mjs_val_t factory = mjs_mk_object(mjs);
mjs_set(mjs, factory, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, (void*)view_descriptor));
mjs_set(mjs, factory, "make", ~0, MJS_MK_FN(js_gui_vf_make));
mjs_set(mjs, factory, "makeWith", ~0, MJS_MK_FN(js_gui_vf_make_with));
return factory;
}
extern const ElfApiInterface js_gui_hashtable_api_interface;
static const JsModuleDescriptor js_gui_desc = {
"gui",
js_gui_create,
js_gui_destroy,
&js_gui_hashtable_api_interface,
};
static const FlipperAppPluginDescriptor plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_gui_desc,
};
const FlipperAppPluginDescriptor* js_gui_ep(void) {
return &plugin_descriptor;
}

View File

@@ -0,0 +1,116 @@
#include "../../js_modules.h"
#include <gui/view.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
JsViewPropTypeString,
JsViewPropTypeNumber,
JsViewPropTypeArr,
} JsViewPropType;
typedef union {
const char* string;
int32_t number;
mjs_val_t array;
} JsViewPropValue;
/**
* @brief Assigns a value to a view property
*
* The name and the type are implicit and defined in the property descriptor
*/
typedef bool (
*JsViewPropAssign)(struct mjs* mjs, void* specific_view, JsViewPropValue value, void* context);
/** @brief Property descriptor */
typedef struct {
const char* name; //<! Property name, as visible from JS
JsViewPropType type; // <! Property type, ensured by the GUI module
JsViewPropAssign assign; // <! Property assignment callback
} JsViewPropDescriptor;
// View method signatures
/** @brief View's `_alloc` method */
typedef void* (*JsViewAlloc)(void);
/** @brief View's `_get_view` method */
typedef View* (*JsViewGetView)(void* specific_view);
/** @brief View's `_free` method */
typedef void (*JsViewFree)(void* specific_view);
// Glue code method signatures
/** @brief Context instantiation for glue code */
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 Descriptor for a JS view
*
* Contains:
* - Pointers to generic view methods (`alloc`, `get_view` and `free`)
* - Pointers to glue code context ctor/dtor methods (`custom_make`,
* `custom_destroy`)
* - Descriptors of properties visible from JS (`prop_cnt`, `props`)
*
* `js_gui` uses this descriptor to produce view factories and views.
*/
typedef struct {
JsViewAlloc alloc;
JsViewGetView get_view;
JsViewFree free;
JsViewCustomMake custom_make; // <! May be NULL
JsViewCustomDestroy custom_destroy; // <! 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 _________/
/**
* @brief Creates a JS `ViewFactory` object
*
* This function is intended to be used by individual view adapter modules that
* wish to create a unified JS API interface in a declarative way. Usually this
* is done via the `JS_GUI_VIEW_DEF` macro which hides all the boilerplate.
*
* The `ViewFactory` object exposes two methods, `make` and `makeWith`, each
* returning a `View` object. These objects fully comply with the expectations
* of the `ViewDispatcher`, TS type definitions and the proposed Flipper JS
* coding style.
*/
mjs_val_t js_gui_make_view_factory(struct mjs* mjs, const JsViewDescriptor* view_descriptor);
/**
* @brief Defines a module implementing `View` glue code
*/
#define JS_GUI_VIEW_DEF(name, descriptor) \
static void* view_mod_ctor(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { \
UNUSED(modules); \
*object = js_gui_make_view_factory(mjs, descriptor); \
return NULL; \
} \
static const JsModuleDescriptor js_mod_desc = { \
"gui__" #name, \
view_mod_ctor, \
NULL, \
NULL, \
}; \
static const FlipperAppPluginDescriptor plugin_descriptor = { \
.appid = PLUGIN_APP_ID, \
.ep_api_version = PLUGIN_API_VERSION, \
.entry_point = &js_mod_desc, \
}; \
const FlipperAppPluginDescriptor* js_view_##name##_ep(void) { \
return &plugin_descriptor; \
}
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,16 @@
#include <flipper_application/api_hashtable/api_hashtable.h>
#include <flipper_application/api_hashtable/compilesort.hpp>
#include "js_gui_api_table_i.h"
static_assert(!has_hash_collisions(js_gui_api_table), "Detected API method hash collision!");
extern "C" constexpr HashtableApiInterface js_gui_hashtable_api_interface{
{
.api_version_major = 0,
.api_version_minor = 0,
.resolver_callback = &elf_resolve_from_hashtable,
},
js_gui_api_table.cbegin(),
js_gui_api_table.cend(),
};

View File

@@ -0,0 +1,4 @@
#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*))));

View File

@@ -0,0 +1,12 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include <gui/modules/loading.h>
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)loading_alloc,
.free = (JsViewFree)loading_free,
.get_view = (JsViewGetView)loading_get_view,
.prop_cnt = 0,
.props = {},
};
JS_GUI_VIEW_DEF(loading, &view_descriptor);

View File

@@ -0,0 +1,87 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/submenu.h>
#define QUEUE_LEN 2
typedef struct {
FuriMessageQueue* queue;
JsEventLoopContract contract;
} JsSubmenuCtx;
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);
}
void choose_callback(void* context, uint32_t index) {
JsSubmenuCtx* ctx = context;
furi_check(furi_message_queue_put(ctx->queue, &index, 0) == FuriStatusOk);
}
static bool
header_assign(struct mjs* mjs, Submenu* submenu, JsViewPropValue value, void* context) {
UNUSED(mjs);
UNUSED(context);
submenu_set_header(submenu, value.string);
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.array);
for(size_t i = 0; i < len; i++) {
mjs_val_t item = mjs_array_get(mjs, value.array, i);
if(!mjs_is_string(item)) return false;
submenu_add_item(submenu, mjs_get_string(mjs, &item, NULL), i, choose_callback, context);
}
return true;
}
static JsSubmenuCtx* ctx_make(struct mjs* mjs, Submenu* input, mjs_val_t view_obj) {
UNUSED(input);
JsSubmenuCtx* context = malloc(sizeof(JsSubmenuCtx));
context->queue = furi_message_queue_alloc(QUEUE_LEN, 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(Submenu* input, JsSubmenuCtx* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->queue);
furi_message_queue_free(context->queue);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)submenu_alloc,
.free = (JsViewFree)submenu_free,
.get_view = (JsViewGetView)submenu_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 2,
.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,78 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include <gui/modules/text_box.h>
static bool
text_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, FuriString* context) {
UNUSED(mjs);
furi_string_set(context, value.string);
text_box_set_text(text_box, furi_string_get_cstr(context));
return true;
}
static bool font_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, void* context) {
UNUSED(context);
TextBoxFont font;
if(strcasecmp(value.string, "hex") == 0) {
font = TextBoxFontHex;
} else if(strcasecmp(value.string, "text") == 0) {
font = TextBoxFontText;
} else {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "must be one of: \"text\", \"hex\"");
return false;
}
text_box_set_font(text_box, font);
return true;
}
static bool
focus_assign(struct mjs* mjs, TextBox* text_box, JsViewPropValue value, void* context) {
UNUSED(context);
TextBoxFocus focus;
if(strcasecmp(value.string, "start") == 0) {
focus = TextBoxFocusStart;
} else if(strcasecmp(value.string, "end") == 0) {
focus = TextBoxFocusEnd;
} else {
mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "must be one of: \"start\", \"end\"");
return false;
}
text_box_set_focus(text_box, focus);
return true;
}
FuriString* ctx_make(struct mjs* mjs, TextBox* specific_view, mjs_val_t view_obj) {
UNUSED(mjs);
UNUSED(specific_view);
UNUSED(view_obj);
return furi_string_alloc();
}
void ctx_destroy(TextBox* specific_view, FuriString* context, FuriEventLoop* loop) {
UNUSED(specific_view);
UNUSED(loop);
furi_string_free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)text_box_alloc,
.free = (JsViewFree)text_box_free,
.get_view = (JsViewGetView)text_box_get_view,
.custom_make = (JsViewCustomMake)ctx_make,
.custom_destroy = (JsViewCustomDestroy)ctx_destroy,
.prop_cnt = 3,
.props = {
(JsViewPropDescriptor){
.name = "text",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)text_assign},
(JsViewPropDescriptor){
.name = "font",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)font_assign},
(JsViewPropDescriptor){
.name = "focus",
.type = JsViewPropTypeString,
.assign = (JsViewPropAssign)focus_assign},
}};
JS_GUI_VIEW_DEF(text_box, &view_descriptor);

View File

@@ -0,0 +1,120 @@
#include "../../js_modules.h" // IWYU pragma: keep
#include "js_gui.h"
#include "../js_event_loop/js_event_loop.h"
#include <gui/modules/text_input.h>
#define DEFAULT_BUF_SZ 33
typedef struct {
char* buffer;
size_t buffer_size;
FuriString* header;
FuriSemaphore* input_semaphore;
JsEventLoopContract contract;
} JsKbdContext;
static mjs_val_t
input_transformer(struct mjs* mjs, FuriSemaphore* semaphore, JsKbdContext* context) {
furi_check(furi_semaphore_acquire(semaphore, 0) == FuriStatusOk);
return mjs_mk_string(mjs, context->buffer, ~0, true);
}
static void input_callback(JsKbdContext* context) {
furi_semaphore_release(context->input_semaphore);
}
static bool
header_assign(struct mjs* mjs, TextInput* input, JsViewPropValue value, JsKbdContext* context) {
UNUSED(mjs);
furi_string_set(context->header, value.string);
text_input_set_header_text(input, furi_string_get_cstr(context->header));
return true;
}
static bool min_len_assign(
struct mjs* mjs,
TextInput* input,
JsViewPropValue value,
JsKbdContext* context) {
UNUSED(mjs);
UNUSED(context);
text_input_set_minimum_length(input, (size_t)value.number);
return true;
}
static bool max_len_assign(
struct mjs* mjs,
TextInput* input,
JsViewPropValue value,
JsKbdContext* context) {
UNUSED(mjs);
context->buffer_size = (size_t)(value.number + 1);
context->buffer = realloc(context->buffer, context->buffer_size); //-V701
text_input_set_result_callback(
input,
(TextInputCallback)input_callback,
context,
context->buffer,
context->buffer_size,
true);
return true;
}
static JsKbdContext* ctx_make(struct mjs* mjs, TextInput* input, mjs_val_t view_obj) {
UNUSED(input);
JsKbdContext* context = malloc(sizeof(JsKbdContext));
*context = (JsKbdContext){
.buffer_size = DEFAULT_BUF_SZ,
.buffer = malloc(DEFAULT_BUF_SZ),
.header = furi_string_alloc(),
.input_semaphore = furi_semaphore_alloc(1, 0),
};
context->contract = (JsEventLoopContract){
.magic = JsForeignMagic_JsEventLoopContract,
.object_type = JsEventLoopObjectTypeSemaphore,
.object = context->input_semaphore,
.non_timer =
{
.event = FuriEventLoopEventIn,
.transformer = (JsEventLoopTransformer)input_transformer,
.transformer_context = context,
},
};
UNUSED(mjs);
UNUSED(view_obj);
mjs_set(mjs, view_obj, "input", ~0, mjs_mk_foreign(mjs, &context->contract));
return context;
}
static void ctx_destroy(TextInput* input, JsKbdContext* context, FuriEventLoop* loop) {
UNUSED(input);
furi_event_loop_maybe_unsubscribe(loop, context->input_semaphore);
furi_semaphore_free(context->input_semaphore);
furi_string_free(context->header);
free(context->buffer);
free(context);
}
static const JsViewDescriptor view_descriptor = {
.alloc = (JsViewAlloc)text_input_alloc,
.free = (JsViewFree)text_input_free,
.get_view = (JsViewGetView)text_input_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 = "minLength",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)min_len_assign},
(JsViewPropDescriptor){
.name = "maxLength",
.type = JsViewPropTypeNumber,
.assign = (JsViewPropAssign)max_len_assign},
}};
JS_GUI_VIEW_DEF(text_input, &view_descriptor);