mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2026-04-24 03:29:57 -07:00
[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:
129
applications/system/js_app/modules/js_gui/dialog.c
Normal file
129
applications/system/js_app/modules/js_gui/dialog.c
Normal 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);
|
||||
12
applications/system/js_app/modules/js_gui/empty_screen.c
Normal file
12
applications/system/js_app/modules/js_gui/empty_screen.c
Normal 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);
|
||||
348
applications/system/js_app/modules/js_gui/js_gui.c
Normal file
348
applications/system/js_app/modules/js_gui/js_gui.c
Normal 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;
|
||||
}
|
||||
116
applications/system/js_app/modules/js_gui/js_gui.h
Normal file
116
applications/system/js_app/modules/js_gui/js_gui.h
Normal 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
|
||||
@@ -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(),
|
||||
};
|
||||
@@ -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*))));
|
||||
12
applications/system/js_app/modules/js_gui/loading.c
Normal file
12
applications/system/js_app/modules/js_gui/loading.c
Normal 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);
|
||||
87
applications/system/js_app/modules/js_gui/submenu.c
Normal file
87
applications/system/js_app/modules/js_gui/submenu.c
Normal 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);
|
||||
78
applications/system/js_app/modules/js_gui/text_box.c
Normal file
78
applications/system/js_app/modules/js_gui/text_box.c
Normal 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);
|
||||
120
applications/system/js_app/modules/js_gui/text_input.c
Normal file
120
applications/system/js_app/modules/js_gui/text_input.c
Normal 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);
|
||||
Reference in New Issue
Block a user