mirror of
https://github.com/Next-Flip/Momentum-Firmware.git
synced 2025-10-05 11:58:44 +02:00
345 lines
13 KiB
C
345 lines
13 KiB
C
#include "../../js_modules.h" // IWYU pragma: keep
|
|
#include "js_gui.h"
|
|
#include "../js_event_loop/js_event_loop.h"
|
|
#include <gui/modules/widget.h>
|
|
|
|
typedef struct {
|
|
FuriMessageQueue* queue;
|
|
JsEventLoopContract contract;
|
|
} JsWidgetCtx;
|
|
|
|
#define QUEUE_LEN 2
|
|
|
|
typedef struct {
|
|
GuiButtonType key;
|
|
InputType type;
|
|
} JsWidgetButtonEvent;
|
|
|
|
/**
|
|
* @brief Parses position (X and Y) from an element declaration object
|
|
*/
|
|
static bool element_get_position(struct mjs* mjs, mjs_val_t element, int32_t* x, int32_t* y) {
|
|
mjs_val_t x_in = mjs_get(mjs, element, "x", ~0);
|
|
mjs_val_t y_in = mjs_get(mjs, element, "y", ~0);
|
|
if(!mjs_is_number(x_in) || !mjs_is_number(y_in)) return false;
|
|
*x = mjs_get_int32(mjs, x_in);
|
|
*y = mjs_get_int32(mjs, y_in);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Parses size (W and h) from an element declaration object
|
|
*/
|
|
static bool element_get_size(struct mjs* mjs, mjs_val_t element, int32_t* w, int32_t* h) {
|
|
mjs_val_t w_in = mjs_get(mjs, element, "w", ~0);
|
|
mjs_val_t h_in = mjs_get(mjs, element, "h", ~0);
|
|
if(!mjs_is_number(w_in) || !mjs_is_number(h_in)) return false;
|
|
*w = mjs_get_int32(mjs, w_in);
|
|
*h = mjs_get_int32(mjs, h_in);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Parses alignment (V and H) from an element declaration object
|
|
*/
|
|
static bool
|
|
element_get_alignment(struct mjs* mjs, mjs_val_t element, Align* align_v, Align* align_h) {
|
|
mjs_val_t align_in = mjs_get(mjs, element, "align", ~0);
|
|
const char* align = mjs_get_string(mjs, &align_in, NULL);
|
|
if(!align) return false;
|
|
if(strlen(align) != 2) return false;
|
|
|
|
if(align[0] == 't') {
|
|
*align_v = AlignTop;
|
|
} else if(align[0] == 'c') {
|
|
*align_v = AlignCenter;
|
|
} else if(align[0] == 'b') {
|
|
*align_v = AlignBottom;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
if(align[1] == 'l') {
|
|
*align_h = AlignLeft;
|
|
} else if(align[1] == 'm') { // m = middle
|
|
*align_h = AlignCenter;
|
|
} else if(align[1] == 'r') {
|
|
*align_h = AlignRight;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Parses font from an element declaration object
|
|
*/
|
|
static bool element_get_font(struct mjs* mjs, mjs_val_t element, Font* font) {
|
|
mjs_val_t font_in = mjs_get(mjs, element, "font", ~0);
|
|
const char* font_str = mjs_get_string(mjs, &font_in, NULL);
|
|
if(!font_str) return false;
|
|
|
|
if(strcmp(font_str, "primary") == 0) {
|
|
*font = FontPrimary;
|
|
} else if(strcmp(font_str, "secondary") == 0) {
|
|
*font = FontSecondary;
|
|
} else if(strcmp(font_str, "keyboard") == 0) {
|
|
*font = FontKeyboard;
|
|
} else if(strcmp(font_str, "big_numbers") == 0) {
|
|
*font = FontBigNumbers;
|
|
} else {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Parses text from an element declaration object
|
|
*/
|
|
static bool element_get_text(struct mjs* mjs, mjs_val_t element, mjs_val_t* text) {
|
|
*text = mjs_get(mjs, element, "text", ~0);
|
|
return mjs_is_string(*text);
|
|
}
|
|
|
|
/**
|
|
* @brief Widget button element callback
|
|
*/
|
|
static void js_widget_button_callback(GuiButtonType result, InputType type, JsWidgetCtx* context) {
|
|
JsWidgetButtonEvent event = {
|
|
.key = result,
|
|
.type = type,
|
|
};
|
|
furi_check(furi_message_queue_put(context->queue, &event, 0) == FuriStatusOk);
|
|
}
|
|
|
|
#define DESTRUCTURE_OR_RETURN(mjs, child_obj, part, ...) \
|
|
if(!element_get_##part(mjs, child_obj, __VA_ARGS__)) \
|
|
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element " #part);
|
|
|
|
static bool js_widget_add_child(
|
|
struct mjs* mjs,
|
|
Widget* widget,
|
|
JsWidgetCtx* context,
|
|
mjs_val_t child_obj) {
|
|
UNUSED(context);
|
|
if(!mjs_is_object(child_obj))
|
|
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "child must be an object");
|
|
|
|
mjs_val_t element_type_term = mjs_get(mjs, child_obj, "element", ~0);
|
|
const char* element_type = mjs_get_string(mjs, &element_type_term, NULL);
|
|
if(!element_type)
|
|
JS_ERROR_AND_RETURN_VAL(
|
|
mjs, MJS_BAD_ARGS_ERROR, false, "child object must have `element` property");
|
|
|
|
if((strcmp(element_type, "string") == 0) || (strcmp(element_type, "string_multiline") == 0)) {
|
|
int32_t x, y;
|
|
Align align_v, align_h;
|
|
Font font;
|
|
mjs_val_t text;
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, alignment, &align_v, &align_h);
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, font, &font);
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
|
|
if(strcmp(element_type, "string") == 0) {
|
|
widget_add_string_element(
|
|
widget, x, y, align_h, align_v, font, mjs_get_string(mjs, &text, NULL));
|
|
} else {
|
|
widget_add_string_multiline_element(
|
|
widget, x, y, align_h, align_v, font, mjs_get_string(mjs, &text, NULL));
|
|
}
|
|
|
|
} else if(strcmp(element_type, "text_box") == 0) {
|
|
int32_t x, y, w, h;
|
|
Align align_v, align_h;
|
|
Font font;
|
|
mjs_val_t text;
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, alignment, &align_v, &align_h);
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, font, &font);
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
|
|
mjs_val_t strip_to_dots_in = mjs_get(mjs, child_obj, "stripToDots", ~0);
|
|
if(!mjs_is_boolean(strip_to_dots_in))
|
|
JS_ERROR_AND_RETURN_VAL(
|
|
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element stripToDots");
|
|
bool strip_to_dots = mjs_get_bool(mjs, strip_to_dots_in);
|
|
widget_add_text_box_element(
|
|
widget, x, y, w, h, align_h, align_v, mjs_get_string(mjs, &text, NULL), strip_to_dots);
|
|
|
|
} else if(strcmp(element_type, "text_scroll") == 0) {
|
|
int32_t x, y, w, h;
|
|
mjs_val_t text;
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
|
|
widget_add_text_scroll_element(widget, x, y, w, h, mjs_get_string(mjs, &text, NULL));
|
|
|
|
} else if(strcmp(element_type, "button") == 0) {
|
|
mjs_val_t btn_in = mjs_get(mjs, child_obj, "button", ~0);
|
|
const char* btn_name = mjs_get_string(mjs, &btn_in, NULL);
|
|
if(!btn_name)
|
|
JS_ERROR_AND_RETURN_VAL(
|
|
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element button");
|
|
GuiButtonType btn_type;
|
|
if(strcmp(btn_name, "left") == 0) {
|
|
btn_type = GuiButtonTypeLeft;
|
|
} else if(strcmp(btn_name, "center") == 0) {
|
|
btn_type = GuiButtonTypeCenter;
|
|
} else if(strcmp(btn_name, "right") == 0) {
|
|
btn_type = GuiButtonTypeRight;
|
|
} else {
|
|
JS_ERROR_AND_RETURN_VAL(mjs, MJS_BAD_ARGS_ERROR, false, "incorrect button type");
|
|
}
|
|
mjs_val_t text;
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, text, &text);
|
|
widget_add_button_element(
|
|
widget,
|
|
btn_type,
|
|
mjs_get_string(mjs, &text, NULL),
|
|
(ButtonCallback)js_widget_button_callback,
|
|
context);
|
|
|
|
} else if(strcmp(element_type, "icon") == 0) {
|
|
int32_t x, y;
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
|
|
mjs_val_t icon_data_in = mjs_get(mjs, child_obj, "iconData", ~0);
|
|
if(!mjs_is_foreign(icon_data_in))
|
|
JS_ERROR_AND_RETURN_VAL(
|
|
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element iconData");
|
|
const Icon* icon = mjs_get_ptr(mjs, icon_data_in);
|
|
widget_add_icon_element(widget, x, y, icon);
|
|
|
|
} else if(strcmp(element_type, "rect") == 0) {
|
|
int32_t x, y, w, h;
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, size, &w, &h);
|
|
mjs_val_t radius_in = mjs_get(mjs, child_obj, "radius", ~0);
|
|
if(!mjs_is_number(radius_in))
|
|
JS_ERROR_AND_RETURN_VAL(
|
|
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element radius");
|
|
int32_t radius = mjs_get_int32(mjs, radius_in);
|
|
mjs_val_t fill_in = mjs_get(mjs, child_obj, "fill", ~0);
|
|
if(!mjs_is_boolean(fill_in))
|
|
JS_ERROR_AND_RETURN_VAL(
|
|
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element fill");
|
|
int32_t fill = mjs_get_bool(mjs, fill_in);
|
|
widget_add_rect_element(widget, x, y, w, h, radius, fill);
|
|
|
|
} else if(strcmp(element_type, "circle") == 0) {
|
|
int32_t x, y;
|
|
DESTRUCTURE_OR_RETURN(mjs, child_obj, position, &x, &y);
|
|
mjs_val_t radius_in = mjs_get(mjs, child_obj, "radius", ~0);
|
|
if(!mjs_is_number(radius_in))
|
|
JS_ERROR_AND_RETURN_VAL(
|
|
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element radius");
|
|
int32_t radius = mjs_get_int32(mjs, radius_in);
|
|
mjs_val_t fill_in = mjs_get(mjs, child_obj, "fill", ~0);
|
|
if(!mjs_is_boolean(fill_in))
|
|
JS_ERROR_AND_RETURN_VAL(
|
|
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element fill");
|
|
int32_t fill = mjs_get_bool(mjs, fill_in);
|
|
widget_add_circle_element(widget, x, y, radius, fill);
|
|
|
|
} else if(strcmp(element_type, "line") == 0) {
|
|
int32_t x1, y1, x2, y2;
|
|
mjs_val_t x1_in = mjs_get(mjs, child_obj, "x1", ~0);
|
|
mjs_val_t y1_in = mjs_get(mjs, child_obj, "y1", ~0);
|
|
mjs_val_t x2_in = mjs_get(mjs, child_obj, "x2", ~0);
|
|
mjs_val_t y2_in = mjs_get(mjs, child_obj, "y2", ~0);
|
|
if(!mjs_is_number(x1_in) || !mjs_is_number(y1_in) || !mjs_is_number(x2_in) ||
|
|
!mjs_is_number(y2_in))
|
|
JS_ERROR_AND_RETURN_VAL(
|
|
mjs, MJS_BAD_ARGS_ERROR, false, "failed to fetch element positions");
|
|
x1 = mjs_get_int32(mjs, x1_in);
|
|
y1 = mjs_get_int32(mjs, y1_in);
|
|
x2 = mjs_get_int32(mjs, x2_in);
|
|
y2 = mjs_get_int32(mjs, y2_in);
|
|
widget_add_line_element(widget, x1, y1, x2, y2);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void js_widget_reset_children(Widget* widget, void* state) {
|
|
UNUSED(state);
|
|
widget_reset(widget);
|
|
}
|
|
|
|
static mjs_val_t js_widget_button_event_transformer(
|
|
struct mjs* mjs,
|
|
FuriMessageQueue* queue,
|
|
JsWidgetCtx* context) {
|
|
UNUSED(context);
|
|
JsWidgetButtonEvent event;
|
|
furi_check(furi_message_queue_get(queue, &event, 0) == FuriStatusOk);
|
|
const char* event_key;
|
|
if(event.key == GuiButtonTypeLeft) {
|
|
event_key = "left";
|
|
} else if(event.key == GuiButtonTypeCenter) {
|
|
event_key = "center";
|
|
} else if(event.key == GuiButtonTypeRight) {
|
|
event_key = "right";
|
|
} else {
|
|
furi_crash();
|
|
}
|
|
const char* event_type;
|
|
if(event.type == InputTypePress) {
|
|
event_type = "press";
|
|
} else if(event.type == InputTypeRelease) {
|
|
event_type = "release";
|
|
} else if(event.type == InputTypeShort) {
|
|
event_type = "short";
|
|
} else if(event.type == InputTypeLong) {
|
|
event_type = "long";
|
|
} else if(event.type == InputTypeRepeat) {
|
|
event_type = "repeat";
|
|
} else {
|
|
furi_crash();
|
|
}
|
|
mjs_val_t obj = mjs_mk_object(mjs);
|
|
JS_ASSIGN_MULTI(mjs, obj) {
|
|
JS_FIELD("key", mjs_mk_string(mjs, event_key, ~0, true));
|
|
JS_FIELD("type", mjs_mk_string(mjs, event_type, ~0, true));
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
static void* js_widget_custom_make(struct mjs* mjs, Widget* widget, mjs_val_t view_obj) {
|
|
UNUSED(widget);
|
|
JsWidgetCtx* context = malloc(sizeof(JsWidgetCtx));
|
|
context->queue = furi_message_queue_alloc(QUEUE_LEN, sizeof(JsWidgetButtonEvent));
|
|
context->contract = (JsEventLoopContract){
|
|
.magic = JsForeignMagic_JsEventLoopContract,
|
|
.object_type = JsEventLoopObjectTypeQueue,
|
|
.object = context->queue,
|
|
.non_timer =
|
|
{
|
|
.event = FuriEventLoopEventIn,
|
|
.transformer = (JsEventLoopTransformer)js_widget_button_event_transformer,
|
|
},
|
|
};
|
|
mjs_set(mjs, view_obj, "button", ~0, mjs_mk_foreign(mjs, &context->contract));
|
|
return context;
|
|
}
|
|
|
|
static void js_widget_custom_destroy(Widget* widget, JsWidgetCtx* context, FuriEventLoop* loop) {
|
|
UNUSED(widget);
|
|
furi_event_loop_maybe_unsubscribe(loop, context->queue);
|
|
furi_message_queue_free(context->queue);
|
|
free(context);
|
|
}
|
|
|
|
static const JsViewDescriptor view_descriptor = {
|
|
.alloc = (JsViewAlloc)widget_alloc,
|
|
.free = (JsViewFree)widget_free,
|
|
.get_view = (JsViewGetView)widget_get_view,
|
|
.custom_make = (JsViewCustomMake)js_widget_custom_make,
|
|
.custom_destroy = (JsViewCustomDestroy)js_widget_custom_destroy,
|
|
.add_child = (JsViewAddChild)js_widget_add_child,
|
|
.reset_children = (JsViewResetChildren)js_widget_reset_children,
|
|
.prop_cnt = 0,
|
|
.props = {},
|
|
};
|
|
JS_GUI_VIEW_DEF(widget, &view_descriptor);
|