diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index e075ef9c4e..138c0507a9 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -118,3 +118,11 @@ App( requires=["js_app"], sources=["modules/js_textbox.c"], ) + +App( + appid="js_widget", + apptype=FlipperAppType.PLUGIN, + entry_point="js_widget_ep", + requires=["js_app"], + sources=["modules/js_widget.c"], +) diff --git a/applications/system/js_app/examples/apps/Scripts/widget-js.fxbm b/applications/system/js_app/examples/apps/Scripts/widget-js.fxbm new file mode 100644 index 0000000000..9ba5783cec Binary files /dev/null and b/applications/system/js_app/examples/apps/Scripts/widget-js.fxbm differ diff --git a/applications/system/js_app/examples/apps/Scripts/widget.js b/applications/system/js_app/examples/apps/Scripts/widget.js new file mode 100644 index 0000000000..521cfdc840 --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/widget.js @@ -0,0 +1,63 @@ +let widget = require("widget"); + +let demo_seconds = 30; + +print("Loading file", __filepath); +print("From directory", __dirpath); + +// addText supports "Primary" and "Secondary" font sizes. +widget.addText(10, 10, "Primary", "Example JS widget"); +widget.addText(10, 20, "Secondary", "Example widget from JS!"); + +// load a Xbm file from the same directory as this script. +widget.addText(0, 30, "Secondary", __filepath); +let logo = widget.loadImageXbm(__dirpath + "/widget-js.fxbm"); + +// add a line (x1, y1, x2, y2) +widget.addLine(10, 35, 120, 35); + +// add a circle/disc (x, y, radius) +widget.addCircle(12, 52, 10); +widget.addDisc(12, 52, 5); + +// add a frame/box (x, y, width, height) +widget.addFrame(25, 45, 10, 10); +widget.addBox(27, 47, 6, 6); + +// add a rounded frame/box (x, y, width, height, radius) +widget.addRframe(50, 45, 15, 15, 3); +widget.addRbox(53, 48, 6, 6, 2); + +// add a dot (x, y) +widget.addDot(100, 45); +widget.addDot(102, 44); +widget.addDot(104, 43); + +// add an icon (x, y, icon) +widget.addIcon(100, 55, "Common/ButtonUp_7x4"); +widget.addIcon(100, 60, "Common/ButtonDown_7x4"); + +// add a glyph (x, y, glyph) +widget.addGlyph(120, 50, 0x23); + +// Show the widget (drawing the layers in the orderer they were added) +widget.show(); + +let i = 1; +let bitmap = undefined; +while (widget.isOpen() && i <= demo_seconds) { + // Print statements will only show up once the widget is closed. + print("count is at", i++); + + // You can call remove on any added item, it does not impact the other ids. + if (bitmap) { widget.remove(bitmap); bitmap = undefined; } + // All of the addXXX functions return an id that can be used to remove the item. + else { bitmap = widget.addXbm(80, 45, logo); } + + delay(1000); +} + +// If user did not press the back button, close the widget. +if (widget.isOpen()) { + widget.close(); +} \ No newline at end of file diff --git a/applications/system/js_app/modules/js_widget.c b/applications/system/js_app/modules/js_widget.c new file mode 100644 index 0000000000..66f2e549fd --- /dev/null +++ b/applications/system/js_app/modules/js_widget.c @@ -0,0 +1,1019 @@ +#include +#include +#include +#include +#include +#include "../js_modules.h" + +typedef struct WidgetComponent WidgetComponent; +ARRAY_DEF(ComponentArray, WidgetComponent*, M_PTR_OPLIST); + +typedef struct XbmImage XbmImage; +LIST_DEF(XbmImageList, XbmImage*, M_POD_OPLIST); + +struct WidgetComponent { + void (*draw)(Canvas* canvas, void* model); + void (*free)(WidgetComponent* component); + void* model; + uint32_t id; +}; + +struct XbmImage { + uint32_t width; + uint32_t height; + uint8_t data[]; +}; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t w; + uint8_t h; +} BoxElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t r; +} CircleElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t r; +} DiscElement; + +typedef struct { + uint8_t x; + uint8_t y; +} DotElement; + +typedef struct { + uint8_t x; + uint8_t y; + const Icon* icon; +} IconElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t w; + uint8_t h; +} FrameElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint16_t ch; +} GlyphElement; + +typedef struct { + uint8_t x1; + uint8_t y1; + uint8_t x2; + uint8_t y2; +} LineElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t w; + uint8_t h; + uint8_t r; +} RboxElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint8_t w; + uint8_t h; + uint8_t r; +} RframeElement; + +typedef struct { + uint8_t x; + uint8_t y; + Font font; + FuriString* text; +} TextElement; + +typedef struct { + uint8_t x; + uint8_t y; + uint32_t index; + View* view; +} XbmElement; + +typedef struct { + ComponentArray_t component; + XbmImageList_t image; + uint32_t max_assigned_id; +} WidgetModel; + +typedef struct { + View* view; + ViewDispatcher* view_dispatcher; + FuriThread* thread; +} JsWidgetInst; + +static JsWidgetInst* get_this_ctx(struct mjs* mjs) { + mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0); + JsWidgetInst* widget = mjs_get_ptr(mjs, obj_inst); + furi_assert(widget); + return widget; +} + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool check_arg_count(struct mjs* mjs, size_t count) { + size_t num_args = mjs_nargs(mjs); + if(num_args != count) { + ret_bad_args(mjs, "Wrong argument count"); + return false; + } + return true; +} + +static void js_widget_load_image_xbm(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 1)) { + return; + } + + mjs_val_t path_arg = mjs_arg(mjs, 0); + size_t path_len = 0; + const char* path = mjs_get_string(mjs, &path_arg, &path_len); + if(!path) { + ret_bad_args(mjs, "Path must be a string"); + return; + } + + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + XbmImage* xbm = NULL; + + do { + if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) { + ret_bad_args(mjs, "Failed to open file"); + break; + } + + uint32_t size = 0; + if(storage_file_read(file, &size, sizeof(size)) != sizeof(size)) { + ret_bad_args(mjs, "Failed to get file size"); + break; + } + + xbm = malloc(size); + if(storage_file_read(file, xbm, size) != size) { + ret_bad_args(mjs, "Failed to load entire file"); + free(xbm); + xbm = NULL; + break; + } + } while(false); + + storage_file_free(file); + + if(xbm == NULL) { + mjs_return(mjs, MJS_UNDEFINED); + return; + } + + uint32_t count = 0; + with_view_model( + widget->view, + WidgetModel * model, + { + count = XbmImageList_size(model->image); + XbmImageList_push_back(model->image, xbm); + }, + false); + + mjs_return(mjs, mjs_mk_number(mjs, count)); +} + +static void js_widget_remove(struct mjs* mjs) { + bool removed = false; + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 1)) { + return; + } + + with_view_model( + widget->view, + WidgetModel * model, + { + uint32_t id = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + ComponentArray_it_t it; + ComponentArray_it(it, model->component); + while(!ComponentArray_end_p(it)) { + WidgetComponent* component = *ComponentArray_ref(it); + if(component->id == id) { + if(component->free) { + component->free(component); + } + ComponentArray_remove(model->component, it); + removed = true; + break; + } + ComponentArray_next(it); + } + }, + true); + + mjs_return(mjs, mjs_mk_boolean(mjs, removed)); +} + +static void widget_box_draw(Canvas* canvas, void* model) { + BoxElement* element = model; + canvas_draw_box(canvas, element->x, element->y, element->w, element->h); +} + +static void widget_box_free(WidgetComponent* component) { + BoxElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_box(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 4)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t w = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + int32_t h = mjs_get_int32(mjs, mjs_arg(mjs, 3)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_box_draw; + component->free = widget_box_free; + component->model = malloc(sizeof(BoxElement)); + BoxElement* element = component->model; + element->x = x; + element->y = y; + element->w = w; + element->h = h; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_circle_draw(Canvas* canvas, void* model) { + CircleElement* element = model; + canvas_draw_circle(canvas, element->x, element->y, element->r); +} + +static void widget_circle_free(WidgetComponent* component) { + CircleElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_circle(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 3)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t r = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_circle_draw; + component->free = widget_circle_free; + component->model = malloc(sizeof(CircleElement)); + CircleElement* element = component->model; + element->x = x; + element->y = y; + element->r = r; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_disc_draw(Canvas* canvas, void* model) { + DiscElement* element = model; + canvas_draw_disc(canvas, element->x, element->y, element->r); +} + +static void widget_disc_free(WidgetComponent* component) { + DiscElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_disc(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 3)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t r = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_disc_draw; + component->free = widget_disc_free; + component->model = malloc(sizeof(DiscElement)); + DiscElement* element = component->model; + element->x = x; + element->y = y; + element->r = r; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_dot_draw(Canvas* canvas, void* model) { + DotElement* element = model; + canvas_draw_dot(canvas, element->x, element->y); +} + +static void widget_dot_free(WidgetComponent* component) { + DotElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_dot(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 2)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_dot_draw; + component->free = widget_dot_free; + component->model = malloc(sizeof(DotElement)); + DotElement* element = component->model; + element->x = x; + element->y = y; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_frame_draw(Canvas* canvas, void* model) { + FrameElement* element = model; + canvas_draw_frame(canvas, element->x, element->y, element->w, element->h); +} + +static void widget_frame_free(WidgetComponent* component) { + FrameElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_frame(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 4)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t w = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + int32_t h = mjs_get_int32(mjs, mjs_arg(mjs, 3)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_frame_draw; + component->free = widget_frame_free; + component->model = malloc(sizeof(FrameElement)); + FrameElement* element = component->model; + element->x = x; + element->y = y; + element->w = w; + element->h = h; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_glyph_draw(Canvas* canvas, void* model) { + GlyphElement* element = model; + canvas_draw_glyph(canvas, element->x, element->y, element->ch); +} + +static void widget_glyph_free(WidgetComponent* component) { + GlyphElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_glyph(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 3)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t ch = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_glyph_draw; + component->free = widget_glyph_free; + component->model = malloc(sizeof(GlyphElement)); + GlyphElement* element = component->model; + element->x = x; + element->y = y; + element->ch = ch; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_icon_draw(Canvas* canvas, void* model) { + IconElement* element = model; + canvas_draw_icon(canvas, element->x, element->y, element->icon); +} + +static void widget_icon_free(WidgetComponent* component) { + IconElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_icon(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 3)) { + return; + } + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + mjs_val_t icon_arg = mjs_arg(mjs, 2); + size_t icon_len = 0; + const char* icon_name = mjs_get_string(mjs, &icon_arg, &icon_len); + if(!icon_name) { + ret_bad_args(mjs, "Icon name must be a string"); + return; + } + + const Icon* icon = NULL; + for(size_t i = 0; i < ICON_PATHS_COUNT; i++) { + if(ICON_PATHS[i].path != NULL && strnlen(ICON_PATHS[i].path, icon_len + 2) == icon_len && + strncmp(icon_name, ICON_PATHS[i].path, icon_len) == 0) { + icon = ICON_PATHS[i].icon; + break; + } + } + + if(icon == NULL) { + ret_bad_args(mjs, "Unknown icon name"); + return; + } + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_icon_draw; + component->free = widget_icon_free; + component->model = malloc(sizeof(IconElement)); + IconElement* element = component->model; + element->x = x; + element->y = y; + element->icon = icon; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_line_draw(Canvas* canvas, void* model) { + LineElement* element = model; + canvas_draw_line(canvas, element->x1, element->y1, element->x2, element->y2); +} + +static void widget_line_free(WidgetComponent* component) { + LineElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_line(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 4)) return; + + int32_t x1 = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y1 = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t x2 = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + int32_t y2 = mjs_get_int32(mjs, mjs_arg(mjs, 3)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_line_draw; + component->free = widget_line_free; + component->model = malloc(sizeof(LineElement)); + LineElement* element = component->model; + element->x1 = x1; + element->y1 = y1; + element->x2 = x2; + element->y2 = y2; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_rbox_draw(Canvas* canvas, void* model) { + RboxElement* element = model; + canvas_draw_rbox(canvas, element->x, element->y, element->w, element->h, element->r); +} + +static void widget_rbox_free(WidgetComponent* component) { + BoxElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_rbox(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 5)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t w = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + int32_t h = mjs_get_int32(mjs, mjs_arg(mjs, 3)); + int32_t r = mjs_get_int32(mjs, mjs_arg(mjs, 4)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_rbox_draw; + component->free = widget_rbox_free; + component->model = malloc(sizeof(RboxElement)); + RboxElement* element = component->model; + element->x = x; + element->y = y; + element->w = w; + element->h = h; + element->r = r; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_rframe_draw(Canvas* canvas, void* model) { + RframeElement* element = model; + canvas_draw_rframe(canvas, element->x, element->y, element->w, element->h, element->r); +} + +static void widget_rframe_free(WidgetComponent* component) { + RframeElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_rframe(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 5)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t w = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + int32_t h = mjs_get_int32(mjs, mjs_arg(mjs, 3)); + int32_t r = mjs_get_int32(mjs, mjs_arg(mjs, 4)); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_rframe_draw; + component->free = widget_rframe_free; + component->model = malloc(sizeof(RframeElement)); + RframeElement* element = component->model; + element->x = x; + element->y = y; + element->w = w; + element->h = h; + element->r = r; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_text_draw(Canvas* canvas, void* model) { + TextElement* element = model; + canvas_set_font(canvas, element->font); + canvas_draw_str(canvas, element->x, element->y, furi_string_get_cstr(element->text)); +} + +static void widget_text_free(WidgetComponent* component) { + TextElement* element = component->model; + furi_string_free(element->text); + free(element); + free(component); +} + +static void js_widget_add_text(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 4)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + + mjs_val_t font_arg = mjs_arg(mjs, 2); + size_t font_name_len = 0; + const char* font_name_text = mjs_get_string(mjs, &font_arg, &font_name_len); + if(!font_name_text) { + ret_bad_args(mjs, "Font name must be a string"); + return; + } + + Font font = FontTotalNumber; + size_t cmp_str_len = strlen("Primary"); + if(font_name_len == cmp_str_len && strncmp(font_name_text, "Primary", cmp_str_len) == 0) { + font = FontPrimary; + } else { + cmp_str_len = strlen("Secondary"); + if(font_name_len == cmp_str_len && + strncmp(font_name_text, "Secondary", cmp_str_len) == 0) { + font = FontSecondary; + } + } + if(font == FontTotalNumber) { + ret_bad_args(mjs, "Unknown font name"); + return; + } + + mjs_val_t text_arg = mjs_arg(mjs, 3); + size_t text_len = 0; + const char* text = mjs_get_string(mjs, &text_arg, &text_len); + if(!text) { + ret_bad_args(mjs, "Text must be a string"); + return; + } + FuriString* text_str = furi_string_alloc_set(text); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_text_draw; + component->free = widget_text_free; + component->model = malloc(sizeof(TextElement)); + TextElement* element = component->model; + element->x = x; + element->y = y; + element->font = font; + element->text = text_str; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void widget_xbm_draw(Canvas* canvas, void* model) { + XbmElement* element = model; + XbmImage* image = NULL; + + with_view_model( + element->view, + WidgetModel * widget_model, + { image = *XbmImageList_get(widget_model->image, element->index); }, + false); + + if(image) { + canvas_draw_xbm(canvas, element->x, element->y, image->width, image->height, image->data); + } +} + +static void widget_xbm_free(WidgetComponent* component) { + XbmElement* element = component->model; + free(element); + free(component); +} + +static void js_widget_add_xbm(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 3)) return; + + int32_t x = mjs_get_int32(mjs, mjs_arg(mjs, 0)); + int32_t y = mjs_get_int32(mjs, mjs_arg(mjs, 1)); + int32_t index = mjs_get_int32(mjs, mjs_arg(mjs, 2)); + with_view_model( + widget->view, + WidgetModel * widget_model, + { + size_t count = XbmImageList_size(widget_model->image); + if(index < 0 || index >= (int32_t)count) { + ret_bad_args(mjs, "Invalid image index"); + return; + } + }, + false); + + WidgetComponent* component = malloc(sizeof(WidgetComponent)); + component->draw = widget_xbm_draw; + component->free = widget_xbm_free; + component->model = malloc(sizeof(XbmElement)); + XbmElement* element = component->model; + element->x = x; + element->y = y; + element->index = index; + + with_view_model( + widget->view, + WidgetModel * model, + { + ++model->max_assigned_id; + component->id = model->max_assigned_id; + element->view = widget->view; + ComponentArray_push_back(model->component, component); + }, + true); + + mjs_return(mjs, mjs_mk_number(mjs, component->id)); +} + +static void js_widget_is_open(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 0)) return; + + mjs_return(mjs, mjs_mk_boolean(mjs, !!widget->thread)); +} + +static void widget_deinit(void* context) { + JsWidgetInst* widget = context; + if(widget->thread) { + furi_thread_join(widget->thread); + furi_thread_free(widget->thread); + widget->thread = NULL; + + furi_assert(widget->view_dispatcher); + view_dispatcher_remove_view(widget->view_dispatcher, 0); + view_dispatcher_free(widget->view_dispatcher); + widget->view_dispatcher = NULL; + + furi_record_close(RECORD_GUI); + } +} + +static void widget_callback(void* context, uint32_t arg) { + UNUSED(arg); + widget_deinit(context); +} + +static bool widget_exit(void* context) { + JsWidgetInst* widget = context; + view_dispatcher_stop(widget->view_dispatcher); + furi_timer_pending_callback(widget_callback, widget, 0); + return true; +} + +static int32_t widget_thread(void* context) { + ViewDispatcher* view_dispatcher = context; + view_dispatcher_run(view_dispatcher); + return 0; +} + +static void js_widget_show(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 0)) return; + + Gui* gui = furi_record_open(RECORD_GUI); + + widget->view_dispatcher = view_dispatcher_alloc(); + view_dispatcher_enable_queue(widget->view_dispatcher); + view_dispatcher_add_view(widget->view_dispatcher, 0, widget->view); + view_dispatcher_set_event_callback_context(widget->view_dispatcher, widget); + view_dispatcher_set_navigation_event_callback(widget->view_dispatcher, widget_exit); + view_dispatcher_attach_to_gui(widget->view_dispatcher, gui, ViewDispatcherTypeFullscreen); + view_dispatcher_switch_to_view(widget->view_dispatcher, 0); + + widget->thread = + furi_thread_alloc_ex("JsWidget", 1024, widget_thread, widget->view_dispatcher); + furi_thread_start(widget->thread); + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void js_widget_close(struct mjs* mjs) { + JsWidgetInst* widget = get_this_ctx(mjs); + if(!check_arg_count(mjs, 0)) return; + + if(widget->thread) { + view_dispatcher_stop(widget->view_dispatcher); + widget_deinit(widget); + } + + mjs_return(mjs, MJS_UNDEFINED); +} + +static void widget_draw_callback(Canvas* canvas, void* model) { + WidgetModel* widget_model = model; + canvas_clear(canvas); + + ComponentArray_it_t it; + ComponentArray_it(it, widget_model->component); + while(!ComponentArray_end_p(it)) { + WidgetComponent* component = *ComponentArray_ref(it); + if(component->draw != NULL) { + component->draw(canvas, component->model); + } + ComponentArray_next(it); + } +} + +static void widget_remove_view(void* context) { + JsWidgetInst* widget = context; + + if(widget->view) { + with_view_model( + widget->view, + WidgetModel * model, + { + ComponentArray_it_t it; + ComponentArray_it(it, model->component); + while(!ComponentArray_end_p(it)) { + WidgetComponent* component = *ComponentArray_ref(it); + if(component->free) { + component->free(component); + component->free = NULL; + } + ComponentArray_next(it); + } + ComponentArray_reset(model->component); + ComponentArray_clear(model->component); + }, + false); + with_view_model( + widget->view, WidgetModel * model, { XbmImageList_clear(model->image); }, false); + view_free(widget->view); + widget->view = NULL; + } +} + +static JsWidgetInst* widget_alloc(void) { + JsWidgetInst* widget = malloc(sizeof(JsWidgetInst)); + widget->thread = NULL; + widget->view_dispatcher = NULL; + + widget->view = view_alloc(); + view_allocate_model(widget->view, ViewModelTypeLockFree, sizeof(WidgetModel)); + view_set_draw_callback(widget->view, widget_draw_callback); + with_view_model( + widget->view, + WidgetModel * model, + { + ComponentArray_init(model->component); + XbmImageList_init(model->image); + model->max_assigned_id = 0; + }, + true); + + return widget; +} + +static void* js_widget_create(struct mjs* mjs, mjs_val_t* object) { + JsWidgetInst* widget = widget_alloc(); + mjs_val_t widget_obj = mjs_mk_object(mjs); + mjs_set(mjs, widget_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, widget)); + // addBox(x: number, y: number, w: number, h: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addBox", ~0, MJS_MK_FN(js_widget_add_box)); + // addCircle(x: number, y: number, r: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addCircle", ~0, MJS_MK_FN(js_widget_add_circle)); + // addDisc(x: number, y: number, r: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addDisc", ~0, MJS_MK_FN(js_widget_add_disc)); + // addDot(x: number, y: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addDot", ~0, MJS_MK_FN(js_widget_add_dot)); + // addFrame(x: number, y: number, w: number, h: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addFrame", ~0, MJS_MK_FN(js_widget_add_frame)); + // addGlyph(x: number, y: number, ch: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addGlyph", ~0, MJS_MK_FN(js_widget_add_glyph)); + // addIcon(x: number, y: number, icon: string): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addIcon", ~0, MJS_MK_FN(js_widget_add_icon)); + // addLine(x1: number, y1: number, x2: number, y2: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addLine", ~0, MJS_MK_FN(js_widget_add_line)); + // addRbox(x: number, y: number, w: number, h: number, r: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addRbox", ~0, MJS_MK_FN(js_widget_add_rbox)); + // addRframe(x: number, y: number, w: number, h: number, r: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addRframe", ~0, MJS_MK_FN(js_widget_add_rframe)); + // addText(x: number, y: number, font: string, text: string): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addText", ~0, MJS_MK_FN(js_widget_add_text)); + // addXbm(x: number, y: number, index: number): number (returns id of the added component) + mjs_set(mjs, widget_obj, "addXbm", ~0, MJS_MK_FN(js_widget_add_xbm)); + // loadImageXbm(path: string): number (returns index of the loaded image) + mjs_set(mjs, widget_obj, "loadImageXbm", ~0, MJS_MK_FN(js_widget_load_image_xbm)); + // remove(id: number): boolean (returns true if the component was removed) + mjs_set(mjs, widget_obj, "remove", ~0, MJS_MK_FN(js_widget_remove)); + // isOpen(): boolean (returns true if the widget is open) + mjs_set(mjs, widget_obj, "isOpen", ~0, MJS_MK_FN(js_widget_is_open)); + // show(): void (shows the widget) + mjs_set(mjs, widget_obj, "show", ~0, MJS_MK_FN(js_widget_show)); + // close(): void (closes the widget) + mjs_set(mjs, widget_obj, "close", ~0, MJS_MK_FN(js_widget_close)); + *object = widget_obj; + return widget; +} + +static void js_widget_destroy(void* inst) { + JsWidgetInst* widget = inst; + if(widget->thread) { + view_dispatcher_stop(widget->view_dispatcher); + widget_deinit(widget); + } + widget_remove_view(widget); + free(widget); +} + +static const JsModuleDescriptor js_widget_desc = { + "widget", + js_widget_create, + js_widget_destroy, +}; + +static const FlipperAppPluginDescriptor widget_plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_widget_desc, +}; + +const FlipperAppPluginDescriptor* js_widget_ep(void) { + return &widget_plugin_descriptor; +}